「 不懂就問(wèn) 」Esbuild 為什么這么快?
前言
esbuild 是新一代的 JavaScript 打包工具。
他的作者是 Figma 的 CTO - Evan Wallace。
( 這卡姿蘭大眼睛,令人唏噓的發(fā)際線, 一看就知道很強(qiáng)!)
esbuild以速度快而著稱,耗時(shí)只有 webpack 的 2% ~3%。
esbuild 項(xiàng)目主要目標(biāo)是: 開辟一個(gè)構(gòu)建工具性能的新時(shí)代,創(chuàng)建一個(gè)易用的現(xiàn)代打包器。
它的主要功能:
- 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
現(xiàn)在很多工具都內(nèi)置了它,比如我們熟知的:
- vite,
- snowpack
借助 esbuild 優(yōu)異的性能, vite 更是如虎添翼, 快到飛起。
今天我們就來(lái)探索一下: 為什么 esbuild 這么快?
下文的主要內(nèi)容:
- 幾組性能數(shù)據(jù)對(duì)比
- 為什么 esbuild 這么快
- esbuild upcoming roadmap
- esbuild 在 vite 中的運(yùn)用
- 為什么生產(chǎn)環(huán)境仍需打包?
- 為何vite不用 esbuild 打包?
- 總結(jié)
正文
先看一組對(duì)比:
使用 10 份 threeJS 的生產(chǎn)包,對(duì)比不同打包工具在默認(rèn)配置下的打包速度。
webpack5 墊底, 耗時(shí) 55.25秒。
esbuild 僅耗時(shí) 0.37 秒。
差異巨大。
還有更多對(duì)比:
https://twitter.com/evanwallace/status/1314121407903617025
webpack5 表示很受傷: 我還比不過(guò) webpack 4 ?
...
為什么 esbuild 這么快 ?
有以下幾個(gè)原因。
(為了保證內(nèi)容的準(zhǔn)確性, 以下內(nèi)容翻譯自 esbuild 官網(wǎng)。)
1. 它是用 Go 語(yǔ)言編寫的,并可以編譯為本地代碼。
大多數(shù)打包器都是用 JavaScript 編寫的,但是對(duì)于 JIT 編譯的語(yǔ)言來(lái)說(shuō),命令行應(yīng)用程序擁有最差的性能表現(xiàn)。
每次運(yùn)行打包器時(shí),JavaScript VM 都會(huì)在沒(méi)有任何優(yōu)化提示的情況下看到打包程序的代碼。
在 esbuild 忙于解析 JavaScript 時(shí),node 忙于解析打包程序的JavaScript。
到節(jié)點(diǎn)完成解析打包程序代碼的時(shí)間時(shí),esbuild可能已經(jīng)退出,您的打包程序甚至還沒(méi)有開始打包。
另外,Go 是為并行性而設(shè)計(jì)的,而 JavaScript 不是。
Go在線程之間共享內(nèi)存,而JavaScript必須在線程之間序列化數(shù)據(jù)。
Go 和 JavaScript都有并行的垃圾收集器,但是Go的堆在所有線程之間共享,而對(duì)于JavaScript, 每個(gè)JavaScript線程中都有一個(gè)單獨(dú)的堆。
根據(jù)測(cè)試,這似乎將 JavaScript worker 線程的并行能力減少了一半,大概是因?yàn)橐话隒PU核心正忙于為另一半收集垃圾。
2. 大量使用了并行操作。
esbuild 中的算法經(jīng)過(guò)精心設(shè)計(jì),可以充分利用CPU資源。
大致分為三個(gè)階段:
- 解析
- 鏈接
- 代碼生成
解析和代碼生成是大部分工作,并且可以完全并行化(鏈接在大多數(shù)情況下是固有的串行任務(wù))。
由于所有線程共享內(nèi)存,因此當(dāng)捆綁導(dǎo)入同一JavaScript庫(kù)的不同入口點(diǎn)時(shí),可以輕松地共享工作。
大多數(shù)現(xiàn)代計(jì)算機(jī)具有多內(nèi)核,因此并行性是一個(gè)巨大的勝利。
3. 代碼都是自己寫的, 沒(méi)有使用第三方依賴。
自己編寫所有內(nèi)容, 而不是使用第三方庫(kù),可以帶來(lái)很多性能優(yōu)勢(shì)。
可以從一開始就牢記性能,可以確保所有內(nèi)容都使用一致的數(shù)據(jù)結(jié)構(gòu)來(lái)避免昂貴的轉(zhuǎn)換,并且可以在必要時(shí)進(jìn)行廣泛的體系結(jié)構(gòu)更改。缺點(diǎn)當(dāng)然是多了很多工作。
例如,許多捆綁程序都使用官方的TypeScript編譯器作為解析器。
但是,它是為實(shí)現(xiàn)TypeScript編譯器團(tuán)隊(duì)的目標(biāo)而構(gòu)建的,它們沒(méi)有將性能作為頭等大事。
4. 內(nèi)存的高效利用。
理想情況下, 根據(jù)數(shù)據(jù)數(shù)據(jù)的長(zhǎng)度, 編譯器的復(fù)雜度為O(n).
如果要處理大量數(shù)據(jù),內(nèi)存訪問(wèn)速度可能會(huì)嚴(yán)重影響性能。
對(duì)數(shù)據(jù)進(jìn)行的遍歷次數(shù)越少(將數(shù)據(jù)轉(zhuǎn)換成數(shù)據(jù)所需的不同表示形式也就越少),編譯器就會(huì)越快。
例如,esbuild 僅觸及整個(gè)JavaScript AST 3次:
- 進(jìn)行詞法分析,解析,作用域設(shè)置和聲明符號(hào)的過(guò)程
- 綁定符號(hào),最小化語(yǔ)法。比如:將 JSX / TS轉(zhuǎn)換為 JS, ES Next 轉(zhuǎn)換為 es5。
- 最小標(biāo)識(shí)符,最小空格,生成代碼。
當(dāng) AST 數(shù)據(jù)在CPU緩存中仍然處于活躍狀態(tài)時(shí),會(huì)最大化AST數(shù)據(jù)的重用。
其他打包器在單獨(dú)的過(guò)程中執(zhí)行這些步驟,而不是將它們交織在一起。
它們也可以在數(shù)據(jù)表示之間進(jìn)行轉(zhuǎn)換,將多個(gè)庫(kù)組織在一起(例如:字符串→TS→JS→字符串,然后字符串→JS→舊的JS→字符串,然后字符串→JS→minified JS→字符串)。
這樣會(huì)占用更多內(nèi)存,并且會(huì)減慢速度。
Go的另一個(gè)好處是它可以將內(nèi)容緊湊地存儲(chǔ)在內(nèi)存中,從而使它可以使用更少的內(nèi)存并在CPU緩存中容納更多內(nèi)容。
所有對(duì)象字段的類型和字段都緊密地包裝在一起,例如幾個(gè)布爾標(biāo)志每個(gè)僅占用一個(gè)字節(jié)。
Go 還具有值語(yǔ)義,可以將一個(gè)對(duì)象直接嵌入到另一個(gè)對(duì)象中,因此它是'免費(fèi)的',無(wú)需另外分配。
JavaScript不具有這些功能,還具有其他缺點(diǎn),例如 JIT 開銷(例如隱藏的類插槽)和低效的表示形式(例如,非整數(shù)與指針堆分配)。
以上的每一條因素, 都能在一定程度上提高編譯速度。
當(dāng)它們共同工作時(shí),效果比當(dāng)今通常使用的其他打包器快幾個(gè)數(shù)量級(jí)。
以上內(nèi)容比較繁瑣,對(duì)此,也有一些網(wǎng)友做了簡(jiǎn)要的總結(jié):
- 它是用 Go 語(yǔ)言編寫的,該語(yǔ)言可以編譯為本地代碼。而且 Go 的執(zhí)行速度很快。一般來(lái)說(shuō),JS 的操作是毫秒級(jí),而 Go 則是納秒級(jí)。
- 解析,生成最終打包文件和生成 source maps 的操作全部完全并行化
- 無(wú)需昂貴的數(shù)據(jù)轉(zhuǎn)換,只需很少的幾步即可完成所有操作
- 該庫(kù)以提高編譯速度為編寫代碼時(shí)的第一原則,并盡量避免不必要的內(nèi)存分配。
僅作參考。
Upcoming roadmap
以下這幾個(gè) feature 已經(jīng)在進(jìn)行中了, 而且是第一優(yōu)先級(jí):
- Code splitting (#16, docs)
- CSS content type (#20, docs)
- Plugin API (#111)
下面這幾個(gè) fearure 比較有潛力, 但是還不確定:
- HTML content type (#31)
- Lowering to ES5 (#297)
- Bundling top-level await (#253)
感興趣的可以保持關(guān)注。
esbuild 在 vite 中的運(yùn)用
vite 中大量使用了 esbuild, 這里簡(jiǎn)單分享兩點(diǎn)。
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))
處理 .ts 文件
https://github.com/vitejs/vite/commit/59035546db7ff4b7020242ba994a5395aac92802
為什么生產(chǎn)環(huán)境仍需打包?
盡管原生 ESM 現(xiàn)在得到了廣泛支持,但由于嵌套導(dǎo)入會(huì)導(dǎo)致額外的網(wǎng)絡(luò)往返,在生產(chǎn)環(huán)境中發(fā)布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。
為了在生產(chǎn)環(huán)境中獲得最佳的加載性能,最好還是將代碼進(jìn)行 tree-shaking、懶加載和 chunk 分割(以獲得更好的緩存)。
要確保開發(fā)服務(wù)器和產(chǎn)品構(gòu)建之間的最佳輸出和行為達(dá)到一致,并不容易。
為解決這個(gè)問(wèn)題,Vite 附帶了一套 構(gòu)建優(yōu)化 的 構(gòu)建命令,開箱即用。
為何 vite 不用 esbuild 打包?
雖然 esbuild 快得驚人,并且已經(jīng)是一個(gè)在構(gòu)建庫(kù)方面比較出色的工具,但一些針對(duì)構(gòu)建應(yīng)用的重要功能仍然還在持續(xù)開發(fā)中 —— 特別是代碼分割和 CSS處理方面。
就目前來(lái)說(shuō),Rollup 在應(yīng)用打包方面, 更加成熟和靈活。
盡管如此,當(dāng)未來(lái)這些功能穩(wěn)定后,也不排除使用 esbuild 作為生產(chǎn)構(gòu)建器的可能。
總結(jié)
esbuild 為構(gòu)建提效帶來(lái)了曙光, 而且 esm 的數(shù)量也在快速增加:
https://twitter.com/skypackjs/status/1113838647487287296
希望 esm 生態(tài)盡快完善起來(lái), 造福前端。
本文轉(zhuǎn)載自微信公眾號(hào)「前端皮小蛋」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端皮小蛋公眾號(hào)。