esbuild 為什么這么快?


前言

esbuild 是新一代的 JavaScript 打包工具。

他的作者是 Figma 的 CTO - Evan Wallace

( 這卡姿蘭大眼睛,令人唏噓的發際線, 一看就知道很強!)

esbuild速度快而著稱,耗時只有 webpack 的 2% ~3%。

esbuild 項目主要目標是: 開辟一個構建工具性能的新時代,創建一個易用的現代打包器

它的主要功能:

  • Extreme speed without needing a cache
  • ES6 and CommonJS modules
  • Tree shaking of ES6 modules
  • An API for JavaScript and Go
  • TypeScript and JSX syntax
  • Source maps
  • Minification
  • Plugins

現在很多工具都內置了它,比如我們熟知的:

  • vite,
  • snowpack

借助 esbuild 優異的性能, vite 更是如虎添翼, 快到飛起。

今天我們就來探索一下: 為什么 esbuild 這么快?

下文的主要內容:

  • 幾組性能數據對比
  • 為什么 esbuild 這么快
  • esbuild upcoming roadmap
  • esbuild 在 vite 中的運用
  • 為什么生產環境仍需打包?
  • 為何vite不用 esbuild 打包?
  • 總結

正文

先看一組對比:

使用 10 份 threeJS 的生產包,對比不同打包工具在默認配置下的打包速度。

webpack5 墊底, 耗時 55.25秒。

esbuild 僅耗時 0.37 秒。

差異巨大。

還有更多對比:

https://twitter.com/evanwallace/status/1314121407903617025

webpack5 表示很受傷: 我還比不過 webpack 4 ?

...

為什么 esbuild 這么快 ?

有以下幾個原因。

(為了保證內容的准確性, 以下內容翻譯自 esbuild 官網。)

1. 它是用 Go 語言編寫的,並可以編譯為本地代碼。

大多數打包器都是用 JavaScript 編寫的,但是對於 JIT 編譯的語言來說,命令行應用程序擁有最差的性能表現。

每次運行打包器時,JavaScript VM 都會在沒有任何優化提示的情況下看到打包程序的代碼。

在 esbuild 忙於解析 JavaScript 時,node 忙於解析打包程序的JavaScript。

到節點完成解析打包程序代碼的時間時,esbuild可能已經退出,您的打包程序甚至還沒有開始打包。

另外,Go 是為並行性而設計的,而 JavaScript 不是。

Go在線程之間共享內存,而JavaScript必須在線程之間序列化數據。

Go 和 JavaScript都有並行的垃圾收集器,但是Go的堆在所有線程之間共享,而對於JavaScript, 每個JavaScript線程中都有一個單獨的堆

根據測試,這似乎將 JavaScript worker 線程的並行能力減少了一半,大概是因為一半CPU核心正忙於為另一半收集垃圾。

2. 大量使用了並行操作。

esbuild 中的算法經過精心設計,可以充分利用CPU資源。

大致分為三個階段:

  1. 解析
  2. 鏈接
  3. 代碼生成

解析代碼生成是大部分工作,並且可以完全並行化(鏈接在大多數情況下是固有的串行任務)。

由於所有線程共享內存,因此當捆綁導入同一JavaScript庫的不同入口點時,可以輕松地共享工作。

大多數現代計算機具有多內核,因此並行性是一個巨大的勝利。

3. 代碼都是自己寫的, 沒有使用第三方依賴。

自己編寫所有內容, 而不是使用第三方庫,可以帶來很多性能優勢。

可以從一開始就牢記性能,可以確保所有內容都使用一致的數據結構來避免昂貴的轉換,並且可以在必要時進行廣泛的體系結構更改。缺點當然是多了很多工作。

例如,許多捆綁程序都使用官方的TypeScript編譯器作為解析器。

但是,它是為實現TypeScript編譯器團隊的目標而構建的,它們沒有將性能作為頭等大事。

4. 內存的高效利用。

理想情況下, 根據數據數據的長度, 編譯器的復雜度為O(n).

如果要處理大量數據,內存訪問速度可能會嚴重影響性能。

對數據進行的遍歷次數越少(將數據轉換成數據所需的不同表示形式也就越少),編譯器就會越快。

例如,esbuild 僅觸及整個JavaScript AST 3次:

  1. 進行詞法分析,解析,作用域設置和聲明符號的過程
  2. 綁定符號,最小化語法。比如:將 JSX / TS轉換為 JS, ES Next 轉換為 es5。
  3. 最小標識符,最小空格,生成代碼。

當 AST 數據在CPU緩存中仍然處於活躍狀態時,會最大化AST數據的重用。

其他打包器在單獨的過程中執行這些步驟,而不是將它們交織在一起。

它們也可以在數據表示之間進行轉換,將多個庫組織在一起(例如:字符串→TS→JS→字符串,然后字符串→JS→舊的JS→字符串,然后字符串→JS→minified JS→字符串)。

這樣會占用更多內存,並且會減慢速度。

Go的另一個好處是它可以將內容緊湊地存儲在內存中,從而使它可以使用更少的內存並在CPU緩存中容納更多內容。

所有對象字段的類型和字段都緊密地包裝在一起,例如幾個布爾標志每個僅占用一個字節。

Go 還具有值語義,可以將一個對象直接嵌入到另一個對象中,因此它是'免費的',無需另外分配。

JavaScript不具有這些功能,還具有其他缺點,例如 JIT 開銷(例如隱藏的類插槽)和低效的表示形式(例如,非整數與指針堆分配)。

以上的每一條因素, 都能在一定程度上提高編譯速度。

當它們共同工作時,效果比當今通常使用的其他打包器快幾個數量級。

以上內容比較繁瑣,對此,也有一些網友做了簡要的總結:

  • 它是用 Go 語言編寫的,該語言可以編譯為本地代碼。而且 Go 的執行速度很快。一般來說,JS 的操作是毫秒級,而 Go 則是納秒級
  • 解析,生成最終打包文件和生成 source maps 的操作全部完全並行化
  • 無需昂貴的數據轉換,只需很少的幾步即可完成所有操作
  • 該庫以提高編譯速度為編寫代碼時的第一原則,並盡量避免不必要的內存分配。

僅作參考。

Upcoming roadmap

以下這幾個 feature 已經在進行中了, 而且是第一優先級:

  1. Code splitting (#16, docs)
  2. CSS content type (#20, docs)
  3. Plugin API (#111)

下面這幾個 fearure 比較有潛力, 但是還不確定:

  1. HTML content type (#31)
  2. Lowering to ES5 (#297)
  3. Bundling top-level await (#253)

感興趣的可以保持關注。

esbuild 在 vite 中的運用

vite 中大量使用了 esbuild, 這里簡單分享兩點。

  1. optimizer

https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/index.ts#L262

import { build, BuildOptions as EsbuildBuildOptions } from 'esbuild' // ... const result = await build({ entryPoints: Object.keys(flatIdDeps), bundle: true, format: 'esm', external: config.optimizeDeps?.exclude, logLevel: 'error', splitting: true, sourcemap: true, outdir: cacheDir, treeShaking: 'ignore-annotations', metafile: true, define, plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config) ], ...esbuildOptions }) const meta = result.metafile! // the paths in `meta.outputs` are relative to `process.cwd()` const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) for (const id in deps) { const entry = deps[id] data.optimized[id] = { file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), src: entry, needsInterop: needsInterop( id, idToExports[id], meta.outputs, cacheDirOutputPath ) } } writeFile(dataPath, JSON.stringify(data, null, 2)) 
  1. 處理 .ts 文件

https://github.com/vitejs/vite/commit/59035546db7ff4b7020242ba994a5395aac92802

為什么生產環境仍需打包?

盡管原生 ESM 現在得到了廣泛支持,但由於嵌套導入會導致額外的網絡往返,在生產環境中發布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。

為了在生產環境中獲得最佳的加載性能,最好還是將代碼進行 tree-shaking懶加載chunk 分割(以獲得更好的緩存)。

要確保開發服務器和產品構建之間的最佳輸出行為達到一致,並不容易。

為解決這個問題,Vite 附帶了一套 構建優化構建命令,開箱即用。

為何 vite 不用 esbuild 打包?

雖然 esbuild 快得驚人,並且已經是一個在構建庫方面比較出色的工具,但一些針對構建應用的重要功能仍然還在持續開發中 —— 特別是代碼分割CSS處理方面。

就目前來說,Rollup 在應用打包方面, 更加成熟和靈活。

盡管如此,當未來這些功能穩定后,也不排除使用 esbuild 作為生產構建器的可能。

 

本文為摘錄,僅供學習 感謝騰訊雲:https://cloud.tencent.com/developer/article/1832345 , 如有侵權聯系刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM