探討Esbuild 為什么那么快
Esbuild 是什么
Esbuild 是一個非常新的模塊打包工具,它提供了與 Webpack、Rollup、Parcel 等工具「相似」的資源打包能力,卻有著高的離譜的性能優(yōu)勢:
從上到下,耗時逐步上升達(dá)到數(shù)百倍的差異,這個巨大的性能優(yōu)勢使得 Esbuild 在一眾基于 Node 的構(gòu)建工具中迅速躥紅,特別是 Vite 2.0 宣布使用 Esbuild 預(yù)構(gòu)建依賴后,前端社區(qū)關(guān)于它的討論熱度迅速上升。
那么問題來了,這是怎么做到的?我翻閱了很多資料后,總結(jié)了一些關(guān)鍵因素:
下面展開一一細(xì)講。
為什么快
語言優(yōu)勢
大多數(shù)前端打包工具都是基于 JavaScript 實現(xiàn)的,而 Esbuild 則選擇使用 Go 語言編寫,兩種語言各自有其擅長的場景,但是在資源打包這種 CPU 密集場景下,Go 更具性能優(yōu)勢,差距有多大呢?比如計算 50 次斐波那契數(shù)列,JS 版本:
- function fibonacci(num) {
- if (num < 2) {
- return 1
- }
- return fibonacci(num - 1) + fibonacci(num - 2)
- }
- (() => {
- let cursor = 0;
- while (cursor < 50) {
- fibonacci(cursor++)
- }
- })()
Go 版本:
- package main
- func fibonacci(num int) int{
- if num<2{
- return 1
- }
- return fibonacci(num-1) + fibonacci(num-2)
- }
- func main(){
- for i := 0; i<50; i++{
- fibonacci(i)
- }
- }
JavaScript 版本執(zhí)行耗時大約為 「332.58s」,Go 版本執(zhí)行耗時大約為 「147.08s」,兩者相差約 「1.25」 倍,這個簡單實驗并不能精確定量兩種語言的性能差別,但感官上還是能明顯感知 Go 語言在 CPU 密集場景下會有更好的性能表現(xiàn)。
歸根到底,雖然現(xiàn)代 JS 引擎與10年前相比有巨大的提升,但 JavaScript 本質(zhì)上依然是一門解釋型語言,JavaScript 程序每次執(zhí)行都需要先由解釋器一邊將源碼翻譯成機(jī)器語言,一邊調(diào)度執(zhí)行;而 Go 是一種編譯型語言,在編譯階段就已經(jīng)將源碼轉(zhuǎn)譯為機(jī)器碼,啟動時只需要直接執(zhí)行這些機(jī)器碼即可。
這種語言層面的差異在打包場景下特別突出,說的夸張一點,JavaScript 運行時還在解釋代碼的時候,Esbuild 已經(jīng)在解析用戶代碼;JavaScript 運行時解釋完代碼剛準(zhǔn)備啟動的時候,Esbuild 可能已經(jīng)打包完畢,退出進(jìn)程了!

所以在編譯運行層面,Go 前置了源碼編譯過程,相對 JavaScript 邊解釋邊運行的方式有更高的執(zhí)行性能。
多線程優(yōu)勢
Go 天生具有多線程運行能力,而 JavaScript 本質(zhì)上是一門單線程語言,直到引入 WebWorker 規(guī)范之后才有可能在瀏覽器、Node 中實現(xiàn)多線程操作。
我曾經(jīng)研讀過 Rollup、Webpack 的代碼,就我熟知的范圍內(nèi)兩者均未使用 WebWorker 提供的多線程能力。反觀 Esbuild,它最核心的賣點就是性能,它的實現(xiàn)算法經(jīng)過非常精心的設(shè)計,盡可能飽和地使用各個 CPU 核,特別是打包過程的解析、代碼生成階段已經(jīng)實現(xiàn)完全并行處理。


除了 CPU 指令運行層面的并行外,Go 語言多個線程之間還能共享相同的內(nèi)存空間,而 JavaScript 的每個線程都有自己獨有的內(nèi)存堆。這意味著 Go 中多個處理單元,例如解析資源 A 的線程,可以直接讀取資源 B 線程的運行結(jié)果,而在 JavaScript 中相同的操作需要調(diào)用通訊接口 woker.postMessage 在線程間復(fù)制數(shù)據(jù)。
所以在運行時層面,Go 擁有天然的多線程能力,更高效的內(nèi)存使用率,也就意味著更高的運行性能。
節(jié)制
對,沒錯,節(jié)制!
Esbuild 并不是另一個 Webpack,它僅僅提供了構(gòu)建一個現(xiàn)代 Web 應(yīng)用所需的最小功能集合,未來也不會大規(guī)模加入我們業(yè)已熟悉的各類構(gòu)建特性。最新版本 Esbuild 的主要功能特性有:
- 支持 js、ts、jsx、css、json、文本、圖片等資源
- 增量更新
- Sourcemap
- 開發(fā)服務(wù)器支持
- 代碼壓縮
- Code split
- Tree shaking
- 插件支持
可以看到,這份列表中支持的資源類型、工程化特性非常少,甚至并不足以支撐一個大型項目的開發(fā)需求。在這之外,官網(wǎng)明確聲明未來沒有計劃支持如下特性:
- Elm, Svelte, Vue, Angular 等代碼文件格式
- Ts 類型檢查
- AST 相關(guān)操作 API
- Hot Module Replace
- Module Federation
而且,Esbuild 所設(shè)計的插件系統(tǒng)也無意覆蓋以上這些場景,這就意味著第三方開發(fā)者無法通過「插件」這種無侵入的方式實現(xiàn)上述功能,emmm,可以預(yù)見未來可能會出現(xiàn)很多魔改版本。
Esbuild 只解決一部分問題,所以它的架構(gòu)復(fù)雜度相對較小,相對地編碼復(fù)雜度也會小很多,相對于 Webpack、Rollup 等大一統(tǒng)的工具,也自然更容易把性能做到極致。節(jié)制的功能設(shè)計還能帶來另外一個好處:完全為性能定制的各種附加工具。
定制
回顧一下,在 Webpack、Rollup 這類工具中,我們不得不使用很多額外的第三方插件來解決各種工程需求,比如:
- 使用 babel 實現(xiàn) ES 版本轉(zhuǎn)譯
- 使用 eslint 實現(xiàn)代碼檢查
- 使用 TSC 實現(xiàn) ts 代碼轉(zhuǎn)譯與代碼檢查
- 使用 less、stylus、sass 等 css 預(yù)處理工具
我們已經(jīng)完全習(xí)慣了這種方式,甚至覺得事情就應(yīng)該是這樣的,大多數(shù)人可能根本沒有意識到事情可以有另一種解決方案。Esbuild 起了個頭,選擇完全!完全重寫整套編譯流程所需要用到的所有工具!這意味著它需要重寫 js、ts、jsx、json 等資源文件的加載、解析、鏈接、代碼生成邏輯。
開發(fā)成本很高,而且可能被動陷入封閉的風(fēng)險,但收益也是巨大的,它可以一路貫徹原則,以性能為最高優(yōu)先級定制編譯的各個階段,比如說:
- 重寫 ts 轉(zhuǎn)譯工具,完全拋棄 ts 類型檢查,只做代碼轉(zhuǎn)換
- 大多數(shù)打包工具把詞法分析、語法分析、符號聲明等步驟拆解為多個高內(nèi)聚低耦合的處理單元,各個模塊職責(zé)分明,可讀性、可維護(hù)性較高。而 Esbuild 則堅持性能第一原則,不惜采用反直覺的設(shè)計模式,將多個處理算法混合在一起降低編譯過程數(shù)據(jù)流轉(zhuǎn)所帶來的性能損耗
- 一致的數(shù)據(jù)結(jié)構(gòu),以及衍生出的高效緩存策略,下一節(jié)細(xì)講
這種深度定制一方面降低了設(shè)計成本,能夠保持編譯鏈條的架構(gòu)一致性;一方面能夠貫徹性能第一的原則,確保每個環(huán)節(jié)以及環(huán)節(jié)之間交互性能的最優(yōu)。雖然伴隨著功能、可讀性、可維護(hù)性層面的的犧牲,但在編譯性能方面幾乎做到了極致。
結(jié)構(gòu)一致性
上一節(jié)我們講到 Esbuild 選擇重寫包括 js、ts、jsx、css 等語言在內(nèi)的轉(zhuǎn)譯工具,所以它更能保證源代碼在編譯步驟之間的結(jié)構(gòu)一致性,比如在 Webpack 中使用 babel-loader 處理 JavaScript 代碼時,可能需要經(jīng)過多次數(shù)據(jù)轉(zhuǎn)換:
- Webpack 讀入源碼,此時為字符串形式
- Babel 解析源碼,轉(zhuǎn)換為 AST 形式
- Babel 將源碼 AST 轉(zhuǎn)換為低版本 AST
- Babel 將低版本 AST generate 為低版本源碼,字符串形式
- Webpack 解析低版本源碼
- Webpack 將多個模塊打包成最終產(chǎn)物
源碼需要經(jīng)歷 string => AST => AST => string => AST => string ,在字符串與 AST 之間反復(fù)橫跳。
而 Esbuild 重寫大多數(shù)轉(zhuǎn)譯工具之后,能夠在多個編譯階段共用相似的 AST 結(jié)構(gòu),盡可能減少字符串到 AST 的結(jié)構(gòu)轉(zhuǎn)換,提升內(nèi)存使用效率。
總結(jié)
單純從編譯性能的維度看,Esbuild 確實完勝世面上所有打包框架,差距甚至能在百倍之大:
但這是有代價的,刨除語言層面的天然優(yōu)勢外,在功能層面它直接放棄對 less、stylus、sass、vue、angular 等資源的支持,放棄 MF、HMR、TS 類型檢查等功能,正如作者所說:
❝This will involve saying "no" to requests for adding major features to esbuild itself. I don't think esbuild should become an all-in-one solution for all frontend needs!❞
在我看來,Esbuild 當(dāng)下與未來都不能替代 Webpack,它不適合直接用于生產(chǎn)環(huán)境,而更適合作為一種偏底層的模塊打包工具,需要在它的基礎(chǔ)上二次封裝,擴(kuò)展出一套既兼顧性能又有完備工程化能力的工具鏈,例如 Snowpack, Vite, SvelteKit, Remix Run 等。
總的來說,Esbuild 提供了一種新的設(shè)計思路,值得學(xué)習(xí)了解,但對大多數(shù)業(yè)務(wù)場景還不適合直接投入生產(chǎn)使用。