全文 2500 字,閱讀耗時約 20 分鐘,歡迎點贊關注轉發。
Esbuild 是一個很是新的模塊打包工具,它提供了與 Webpack、Rollup、Parcel 等工具類似的資源打包能力,卻有着高的離譜的性能優點:javascript
從上到下,耗時逐步上升達到數百倍的差別,這個巨大的性能優點使得 Esbuild 在一衆基於 Node 的構建工具中迅速躥紅,特別是 Vite 2.0 宣佈使用 Esbuild 預構建依賴後,前端社區關於它的討論熱度逐漸上升。css
那麼問題來了,這是怎麼作到的?我翻閱了不少資料後,總結了一些關鍵因素:前端
下面展開細講。vue
大多數前端打包工具都是基於 JavaScript 實現的,而 Esbuild 則選擇使用 Go 語言編寫,兩種語言各自有其擅長的場景,可是在資源打包這種 CPU 密集場景下,Go 更具性能優點,差距有多大呢?好比計算 50 次斐波那契數列,JS 版本:java
function fibonacci(num) { if (num < 2) { return 1 } return fibonacci(num - 1) + fibonacci(num - 2) } (() => { let cursor = 0; while (cursor < 50) { fibonacci(cursor++) } })()
Go 版本:webpack
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 版本執行耗時大約爲 332.58s,Go 版本執行耗時大約爲 147.08s,二者相差約 1.25 倍,這個簡單實驗並不能精肯定量兩種語言的性能差異,但感官上仍是能明顯感知 Go 語言在 CPU 密集場景下會有更好的性能表現。git
歸根到底,雖然現代 JS 引擎與10年前相比有巨大的提高,但 JavaScript 本質上依然是一門解釋型語言,JavaScript 程序每次執行都須要先由解釋器一邊將源碼翻譯成機器語言,一邊調度執行;而 Go 是一種編譯型語言,在編譯階段就已經將源碼轉譯爲機器碼,啓動時只須要直接執行這些機器碼便可。也就意味着,Go 語言編寫的程序比 JavaScript 少了一個動態解釋的過程。github
這種語言層面的差別在打包場景下特別突出,說的誇張一點,JavaScript 運行時還在解釋代碼的時候,Esbuild 已經在解析用戶代碼;JavaScript 運行時解釋完代碼剛準備啓動的時候,Esbuild 可能已經打包完畢,退出進程了!web
因此在編譯運行層面,Go 前置了源碼編譯過程,相對 JavaScript 邊解釋邊運行的方式有更高的執行性能。算法
Go 天生具備多線程運行能力,而 JavaScript 本質上是一門單線程語言,直到引入 WebWorker 規範以後纔有可能在瀏覽器、Node 中實現多線程操做。
我曾經研讀過 Rollup、Webpack 的代碼,就我熟知的範圍內二者均未使用 WebWorker 提供的多線程能力。反觀 Esbuild,它最核心的賣點就是性能,它的實現算法通過很是精心的設計,儘量飽和地使用各個 CPU 核,特別是打包過程的解析、代碼生成階段已經實現徹底並行處理。
除了 CPU 指令運行層面的並行外,Go 語言多個線程之間還能共享相同的內存空間,而 JavaScript 的每一個線程都有本身獨有的內存堆。這意味着 Go 中多個處理單元,例如解析資源 A 的線程,能夠直接讀取資源 B 線程的運行結果,而在 JavaScript 中相同的操做須要調用通信接口 woker.postMessage
在線程間複製數據。
因此在運行時層面,Go 擁有自然的多線程能力,更高效的內存使用率,也就意味着更高的運行性能。
對,沒錯,節制!
Esbuild 並非另外一個 Webpack,它僅僅提供了構建一個現代 Web 應用所需的最小功能集合,將來也不會大規模加入咱們業已熟悉的各種構建特性。最新版本 Esbuild 的主要功能特性有:
能夠看到,這份列表中支持的資源類型、工程化特性很是少,甚至並不足以支撐一個大型項目的開發需求。在這以外,官網明確聲明將來沒有計劃支持以下特性:
並且,Esbuild 所設計的插件系統也無心覆蓋以上這些場景,這就意味着第三方開發者沒法經過插件這種無侵入的方式實現上述功能,emmm,能夠預見將來可能會出現不少魔改版本。
Esbuild 只解決一部分問題,因此它的架構複雜度相對較小,相對地編碼複雜度也會小不少,相對於 Webpack、Rollup 等大一統的工具,也天然更容易把性能作到極致。節制的功能設計還能帶來另一個好處:徹底爲性能定製的各類附加工具。
回顧一下,在 Webpack、Rollup 這類工具中,咱們不得不使用不少額外的第三方插件來解決各類工程需求,好比:
咱們已經徹底習慣了這種方式,甚至以爲事情就應該是這樣的,大多數人可能根本沒有意識到事情能夠有另外一種解決方案。Esbuild 起了個頭,選擇徹底!徹底重寫整套編譯流程所須要用到的全部工具!這意味着它須要重寫 js、ts、jsx、json 等資源文件的加載、解析、連接、代碼生成邏輯。
開發成本很高,並且可能被動陷入封閉的風險,但收益也是巨大的,它能夠一路貫徹原則,以性能爲最高優先級定製編譯的各個階段,好比說:
這種深度定製一方面下降了設計成本,可以保持編譯鏈條的架構一致性;一方面可以貫徹性能第一的原則,確保每一個環節以及環節之間交互性能的最優。雖然伴隨着功能、可讀性、可維護性層面的的犧牲,但在編譯性能方面幾乎作到了極致。
上一節咱們講到 Esbuild 選擇重寫包括 js、ts、jsx、css 等語言在內的轉譯工具,因此它更能保證源代碼在編譯步驟之間的結構一致性,好比在 Webpack 中使用 babel-loader 處理 JavaScript 代碼時,可能須要通過屢次數據轉換:
源碼須要經歷 string => AST => AST => string => AST => string
,在字符串與 AST 之間反覆橫跳。
而 Esbuild 重寫大多數轉譯工具以後,可以在多個編譯階段共用類似的 AST 結構,儘量減小字符串到 AST 的結構轉換,提高內存使用效率。
單純從編譯性能的維度看,Esbuild 確實完勝世面上全部打包框架,差距甚至能在百倍之大:
耗時 | 性能差別 | 速度 | 產物大小 | |
---|---|---|---|---|
esbuild | 0.11s | 1x | 1198.5 kloc/s | 0.97mb |
esbuild (1 thread) | 0.40s | 4x | 329.6 kloc/s | 0.97mb |
webpack 4 | 19.14s | 174x | 6.9 kloc/s | 1.26mb |
parcel 1 | 22.41s | 204x | 5.9 kloc/s | 1.56mb |
webpack 5 | 25.61s | 233x | 5.1 kloc/s | 1.26mb |
parcel 2 | 31.39s | 285x | 4.2 kloc/s | 0.97mb |
但這是有代價的,刨除語言層面的自然優點外,在功能層面它直接放棄對 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 當下與將來都不能替代 Webpack,它不適合直接用於生產環境,而更適合做爲一種偏底層的模塊打包工具,須要在它的基礎上二次封裝,擴展出一套既兼顧性能又有完備工程化能力的工具鏈,例如 Snowpack, Vite, SvelteKit, Remix Run 等。
總的來講,Esbuild 提供了一種新的設計思路,值得學習瞭解,但對大多數業務場景還不適合直接投入生產使用。