「 不懂就問 」esbuild 為什么這么快?

前言
esbuild 是新一代的 JavaScript 打包工具。
他的作者是 Figma 的 CTO - Evan Wallace。

esbuild以速度快而著稱,耗時只有 webpack 的 2% ~3%。
esbuild 項目主要目標是: 開辟一個構建工具性能的新時代,創(chuàng)建一個易用的現(xiàn)代打包器。
它的主要功能:
Extreme speed without needing a cacheES6 and CommonJS modulesTree shaking of ES6 modulesAn API for JavaScript and GoTypeScript and JSX syntaxSource mapsMinificationPlugins
現(xiàn)在很多工具都內置了它,比如我們熟知的:
vite,snowpack
借助 esbuild 優(yōu)異的性能, vite 更是如虎添翼, 快到飛起。
今天我們就來探索一下: 為什么 esbuild 這么快?
下文的主要內容:
幾組性能數(shù)據(jù)對比為什么 esbuild 這么快esbuild upcoming roadmapesbuild 在 vite 中的運用為什么生產(chǎn)環(huán)境仍需打包?為何vite不用 esbuild 打包?總結
正文
先看一組對比:

使用 10 份 threeJS 的生產(chǎn)包,對比不同打包工具在默認配置下的打包速度。
webpack5 墊底, 耗時 55.25秒。
esbuild 僅耗時 0.37 秒。
差異巨大。
還有更多對比:

webpack5 表示很受傷: 我還比不過 webpack 4 ?
...
為什么 esbuild 這么快 ?
有以下幾個原因。
(為了保證內容的準確性, 以下內容翻譯自 esbuild 官網(wǎng)。)
1. 它是用 Go 語言編寫的,并可以編譯為本地代碼。
大多數(shù)打包器都是用 JavaScript 編寫的,但是對于 JIT 編譯的語言來說,命令行應用程序擁有最差的性能表現(xiàn)。
每次運行打包器時,JavaScript VM 都會在沒有任何優(yōu)化提示的情況下看到打包程序的代碼。
在 esbuild 忙于解析 JavaScript 時,node 忙于解析打包程序的JavaScript。
到節(jié)點完成解析打包程序代碼的時間時,esbuild可能已經(jīng)退出,您的打包程序甚至還沒有開始打包。
另外,Go 是為并行性而設計的,而 JavaScript 不是。
Go在線程之間共享內存,而JavaScript必須在線程之間序列化數(shù)據(jù)。
Go 和 JavaScript都有并行的垃圾收集器,但是Go的堆在所有線程之間共享,而對于JavaScript, 每個JavaScript線程中都有一個單獨的堆。
根據(jù)測試,這似乎將 JavaScript worker 線程的并行能力減少了一半,大概是因為一半CPU核心正忙于為另一半收集垃圾。
2. 大量使用了并行操作。
esbuild 中的算法經(jīng)過精心設計,可以充分利用CPU資源。
大致分為三個階段:
解析鏈接代碼生成
解析和代碼生成是大部分工作,并且可以完全并行化(鏈接在大多數(shù)情況下是固有的串行任務)。
由于所有線程共享內存,因此當捆綁導入同一JavaScript庫的不同入口點時,可以輕松地共享工作。
大多數(shù)現(xiàn)代計算機具有多內核,因此并行性是一個巨大的勝利。
3. 代碼都是自己寫的, 沒有使用第三方依賴。
自己編寫所有內容, 而不是使用第三方庫,可以帶來很多性能優(yōu)勢。
可以從一開始就牢記性能,可以確保所有內容都使用一致的數(shù)據(jù)結構來避免昂貴的轉換,并且可以在必要時進行廣泛的體系結構更改。缺點當然是多了很多工作。
例如,許多捆綁程序都使用官方的TypeScript編譯器作為解析器。
但是,它是為實現(xiàn)TypeScript編譯器團隊的目標而構建的,它們沒有將性能作為頭等大事。
4. 內存的高效利用。
理想情況下, 根據(jù)數(shù)據(jù)數(shù)據(jù)的長度, 編譯器的復雜度為O(n).
如果要處理大量數(shù)據(jù),內存訪問速度可能會嚴重影響性能。
對數(shù)據(jù)進行的遍歷次數(shù)越少(將數(shù)據(jù)轉換成數(shù)據(jù)所需的不同表示形式也就越少),編譯器就會越快。
例如,esbuild 僅觸及整個JavaScript AST 3次:
進行詞法分析,解析,作用域設置和聲明符號的過程 綁定符號,最小化語法。比如:將 JSX / TS轉換為 JS, ES Next 轉換為 es5。 最小標識符,最小空格,生成代碼。
當 AST 數(shù)據(jù)在CPU緩存中仍然處于活躍狀態(tài)時,會最大化AST數(shù)據(jù)的重用。
其他打包器在單獨的過程中執(zhí)行這些步驟,而不是將它們交織在一起。
它們也可以在數(shù)據(jù)表示之間進行轉換,將多個庫組織在一起(例如:字符串→TS→JS→字符串,然后字符串→JS→舊的JS→字符串,然后字符串→JS→minified JS→字符串)。
這樣會占用更多內存,并且會減慢速度。
Go的另一個好處是它可以將內容緊湊地存儲在內存中,從而使它可以使用更少的內存并在CPU緩存中容納更多內容。
所有對象字段的類型和字段都緊密地包裝在一起,例如幾個布爾標志每個僅占用一個字節(jié)。
Go 還具有值語義,可以將一個對象直接嵌入到另一個對象中,因此它是'免費的',無需另外分配。
JavaScript不具有這些功能,還具有其他缺點,例如 JIT 開銷(例如隱藏的類插槽)和低效的表示形式(例如,非整數(shù)與指針堆分配)。
以上的每一條因素, 都能在一定程度上提高編譯速度。
當它們共同工作時,效果比當今通常使用的其他打包器快幾個數(shù)量級。
以上內容比較繁瑣,對此,也有一些網(wǎng)友做了簡要的總結:
它是用 Go語言編寫的,該語言可以編譯為本地代碼。而且 Go 的執(zhí)行速度很快。一般來說,JS 的操作是毫秒級,而 Go 則是納秒級。解析,生成最終打包文件和生成 source maps 的操作全部完全并行化無需昂貴的數(shù)據(jù)轉換,只需很少的幾步即可完成所有操作該庫 以提高編譯速度為編寫代碼時的第一原則,并盡量避免不必要的內存分配。
僅作參考。
Upcoming roadmap
以下這幾個 feature 已經(jīng)在進行中了, 而且是第一優(yōu)先級:
Code splitting(#16, docs)CSS content type(#20, docs)Plugin API(#111)
下面這幾個 fearure 比較有潛力, 但是還不確定:
HTML content type(#31)Lowering to ES5(#297)Bundling top-level await(#253)
感興趣的可以保持關注。
esbuild 在 vite 中的運用
vite 中大量使用了 esbuild, 這里簡單分享兩點。
optimizer

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))
處理 .ts文件

為什么生產(chǎn)環(huán)境仍需打包?
盡管原生 ESM 現(xiàn)在得到了廣泛支持,但由于嵌套導入會導致額外的網(wǎng)絡往返,在生產(chǎn)環(huán)境中發(fā)布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。
為了在生產(chǎn)環(huán)境中獲得最佳的加載性能,最好還是將代碼進行 tree-shaking、懶加載和 chunk 分割(以獲得更好的緩存)。
要確保開發(fā)服務器和產(chǎn)品構建之間的最佳輸出和行為達到一致,并不容易。
為解決這個問題,Vite 附帶了一套 構建優(yōu)化 的 構建命令,開箱即用。
為何 vite 不用 esbuild 打包?
雖然 esbuild 快得驚人,并且已經(jīng)是一個在構建庫方面比較出色的工具,但一些針對構建應用的重要功能仍然還在持續(xù)開發(fā)中 —— 特別是代碼分割和 CSS處理方面。
就目前來說,Rollup 在應用打包方面, 更加成熟和靈活。
盡管如此,當未來這些功能穩(wěn)定后,也不排除使用 esbuild 作為生產(chǎn)構建器的可能。
總結
esbuild 為構建提效帶來了曙光, 而且 esm 的數(shù)量也在快速增加:

希望 esm 生態(tài)盡快完善起來, 造福前端。
--
今天的內容就這么多, 希望對大家有所啟發(fā)。
才疏學淺,文章若有錯誤, 歡迎指正, 謝謝。
內推社群
我組建了一個氛圍特別好的騰訊內推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進行面試相關的答疑、聊聊面試的故事、并且在你準備好的時候隨時幫你內推。下方加 winty 好友回復「面試」即可。
