前端构建新世代,Esbuild 原来还能这么玩!
大家好,我是若川。持續(xù)組織了5個(gè)月源碼共讀活動(dòng),感興趣的可以點(diǎn)此加我微信 ruochuan12?參與,每周大家一起學(xué)習(xí)200行左右的源碼,共同進(jìn)步。同時(shí)極力推薦訂閱我寫的《學(xué)習(xí)源碼整體架構(gòu)系列》?包含20余篇源碼文章。
今天分享一篇esbuild的文章~
Hello,我是三元同學(xué)。之前停更了一段時(shí)間,因?yàn)榈昧肆鞲?#xff0c;一直在家養(yǎng)病,沒來得及更新文章,跟讀者朋友們先說聲抱歉~今天給大家?guī)淼氖俏易罱鼘懙脑瓌?chuàng)文章,由于近段時(shí)間一直在研究前端構(gòu)建相關(guān)的領(lǐng)域,像 Esbuild、Vite 這些都接觸得比較多了,而且這些工具現(xiàn)在在前端圈也比較熱門,備受業(yè)界關(guān)注,因此我想我有必要把我研究過的一些東西分享給大家,希望能對(duì)你有所幫助。
什么是 Esbuild?
Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 開發(fā)的一款打包工具,相比傳統(tǒng)的打包工具,主打性能優(yōu)勢(shì),在構(gòu)建速度上可以快 10~100 倍。
架構(gòu)優(yōu)勢(shì)
1. Golang 開發(fā)
采用 Go 語言開發(fā),相比于 單線程 + JIT 性質(zhì)的解釋型語言 ,使用 Go 的優(yōu)勢(shì)在于 :
一方面可以充分利用多線程打包,并且線程之間共享內(nèi)容,而 JS 如果使用多線程還需要有線程通信(postMessage)的開銷;
另一方面直接編譯成機(jī)器碼,而不用像 Node 一樣先將 JS 代碼解析為字節(jié)碼,然后轉(zhuǎn)換為機(jī)器碼,大大節(jié)省了程序運(yùn)行時(shí)間。
2. 多核并行
內(nèi)部打包算法充分利用多核 CPU 優(yōu)勢(shì)。Esbuild 內(nèi)部算法設(shè)計(jì)是經(jīng)過精心設(shè)計(jì)的,盡可能充分利用所有的 CPU 內(nèi)核。所有的步驟盡可能并行,這也是得益于 Go 當(dāng)中多線程共享內(nèi)存的優(yōu)勢(shì),而在 JS 中所有的步驟只能是串行的。
3. 從零造輪子
從零開始造輪子,沒有任何第三方庫的黑盒邏輯,保證極致的代碼性能。
4. 高效利用內(nèi)存
一般而言,在 JS 開發(fā)的傳統(tǒng)打包工具當(dāng)中一般會(huì)頻繁地解析和傳遞 AST 數(shù)據(jù),比如 string -> TS -> JS -> string,這其中會(huì)涉及復(fù)雜的編譯工具鏈,比如 webpack -> babel -> terser,每次接觸到新的工具鏈,都得重新解析 AST,導(dǎo)致大量的內(nèi)存占用。而 Esbuild 中從頭到尾盡可能地復(fù)用一份 AST 節(jié)點(diǎn)數(shù)據(jù),從而大大提高了內(nèi)存的利用效率,提升編譯性能。
與 SWC 對(duì)比
速度
下面拿純 Esbuild 和 SWC 來編譯代碼,作為 Transformer 來轉(zhuǎn)換 800+ 個(gè) tsx 文件,不寫任何的 JS 膠水代碼(如 esbuild-register、esbuild-loader、swc-loader 本身為了適配相應(yīng)的宿主工具,會(huì)寫一堆 JS 膠水代碼,影響判斷)。
| 第一次 | 138 ms | 217 ms | 8640 ms |
| 第二次 | 154 ms | 206 ms | 8400 ms |
| 第三次 | 142 ms | 258 ms | 8480 ms |
| 平均 | 144.7 ms | 227 ms | 8507 ms |
| 耗時(shí)倍率 | x1 | x 1.58 | x 58.8 |
從這個(gè)例子可以看出,Esbuild 與 SWC 在性能上是在一個(gè)量級(jí)的,這里通過倉庫的例子 Esbuild 略快,但不排除其他例子里面 SWC 比 Esbuild 略快的場景。
兼容性
Esbuild 本身的限制,包括如下:
沒有 TS 類型檢查
不能操作 AST
不支持裝飾器語法
產(chǎn)物 target 無法降級(jí)到 ES5 及以下
意味著需要 ES5 產(chǎn)物的場景只用 Esbuild 無法勝任。
相比之下,SWC 的兼容性更好:
產(chǎn)物支持 ES5 格式
支持裝飾器語法
可以通過寫 JS 插件操作 AST
應(yīng)用場景
對(duì)于 Esbuild 和 SWC,很多時(shí)候我們都在對(duì)比兩者的性能而忽略了應(yīng)用場景。對(duì)于前端的構(gòu)建工具來說主要有這樣幾個(gè)垂直的功能:
Bundler
Transformer
Minimizer
從上面的速度和兼容性對(duì)比可以看出,Esbuild 和 SWC 作為 transformer 性能是差不多的,但 Esbuild 兼容性遠(yuǎn)遠(yuǎn)不及 SWC。因此,SWC 作為 Transformer 更勝一籌。
但作為 Bundler 以及 Minimizer,SWC 就顯得捉襟見肘了,首先官方的 swcpack 目前基本處于不可用狀態(tài),Minimizer 方面也非常不成熟,很容易碰到兼容性問題。
而 Esbuild 作為 Bundler 已經(jīng)被 Vite 作為開發(fā)階段的依賴預(yù)打包工具,同時(shí)也被大量用作線上 esm CDN 服務(wù),比如esm.sh等等;作為 Minimizer ,Esbuild 也已足夠成熟,目前已經(jīng)被 Vite 作為 JS 和 CSS 代碼的壓縮工具用上了生產(chǎn)環(huán)境。
綜合來看,SWC 與 Esbuild 的關(guān)系類似于當(dāng)下的 Babel 和 Webpack,前者更適合做兼容性和自定義要求高的 Transformer(比如移動(dòng)端業(yè)務(wù)場景),而后者適合做 Bundler 和 Minimizer,以及兼容性和自定義要求均不高的 Transformer。
插件機(jī)制
esbuild 插件就是一個(gè)對(duì)象,里面有name和setup兩個(gè)屬性,name是插件的名稱,setup是一個(gè)函數(shù),其中入?yún)⑹且粋€(gè) build 對(duì)象,這個(gè)對(duì)象上掛載了一些鉤子可供我們自定義一些構(gòu)建邏輯。以下是一個(gè)簡單的esbuild插件示例:
let?envPlugin?=?{name:?'env',setup(build)?{//?文件解析時(shí)觸發(fā)//?將插件作用域限定于env文件,并為其標(biāo)識(shí)命名空間"env-ns"build.onResolve({?filter:?/^env$/?},?args?=>?({path:?args.path,namespace:?'env-ns',}))//?加載文件時(shí)觸發(fā)//?只有命名空間為"env-ns"的文件才會(huì)被處理//?將process.env對(duì)象反序列化為字符串并交由json-loader處理build.onLoad({?filter:?/.*/,?namespace:?'env-ns'?},?()?=>?({contents:?JSON.stringify(process.env),loader:?'json',}))}, }require('esbuild').build({entryPoints:?['app.js'],bundle:?true,outfile:?'out.js',//?應(yīng)用插件plugins:?[envPlugin], }).catch(()?=>?process.exit(1))使用如下:
*//?應(yīng)用了env插件后,構(gòu)建時(shí)將會(huì)被替換成process.env對(duì)象*import?{?PATH?}?from?'env'console.log(`PATH?is?${PATH}`)不過在編寫插件的時(shí)候有一些需要注意的地方:
Esbuild 插件機(jī)制只可作用于 build API,而不適用于 transformAPI,這意味著 webpack 當(dāng)中的 esbuild-loader 這種只使用 Esbuild transform 功能的地方無法利用 Esbuild 的插件機(jī)制。
插件中的 filter 正則是使用 go 原生的正則實(shí)現(xiàn)的,用來過濾文件,為了不使性能過于劣化,規(guī)則應(yīng)該盡可能嚴(yán)格。同時(shí)它本身和 JS 的正則也有所區(qū)別,比如前瞻(?<=)、后顧(?=)和反向引用(\1)就不支持。
實(shí)際的插件應(yīng)該考慮到自定義緩存(減少 load 的重復(fù)開銷)、sourcemap 合并(源代碼正確映射)和錯(cuò)誤處理。可以參考 Svelte plugin。
虛擬模塊支持
與 Rollup 對(duì)比
作為打包器,一般需要兩種形式的模塊,一種存在于真實(shí)的磁盤文件系統(tǒng)中,另一種并不在磁盤而在內(nèi)存當(dāng)中,也就是虛擬模塊。Rollup 本身就天然支持虛擬模塊,Vite 基于它的插件機(jī)制,也重度使用了虛擬模塊的功能,以 wasm 文件的處理為例:
const?wasmHelperId?=?'/__vite-wasm-helper' //?helper?函數(shù)實(shí)現(xiàn) const?wasmHelper?=?async?(opts?=?{},?url:?string)?=>?{//?省略具體實(shí)現(xiàn) } export?const?wasmPlugin?=?(config:?ResolvedConfig):?Plugin?=>?{return?{name:?'vite:wasm',resolveId(id)?{if?(id?===?wasmHelperId)?{return?id}},async?load(id)?{if?(id?===?wasmHelperId)?{return?`export?default?${wasmHelperCode}`}if?(!id.endsWith('.wasm'))?{return}const?url?=?await?fileToUrl(id,?config,?this)//?虛擬模塊return?` import?initWasm?from?"${wasmHelperId}" export?default?opts?=>?initWasm(opts,?${JSON.stringify(url)}) `}} }但 Rollup 的虛擬模塊也有一些限制,為了與真實(shí)模塊區(qū)分開,默認(rèn)約定要在路徑前面拼上一個(gè)'\0'。這樣會(huì)對(duì)路徑產(chǎn)生一定的入侵性,直接放到瀏覽器進(jìn)行 import 會(huì)出問題(Vite 內(nèi)部也將 \0 替換成 __xx 這種形式,以免直接將 帶\0 路徑放到瀏覽器中 import):
image.pngEsbuild 中對(duì)于虛擬模塊的支持更加友好一些,直接通過 namespace 來區(qū)分真實(shí)模塊和虛擬模塊,這樣也不會(huì)有 \0 這樣 hack 操作。
編譯能力
使用 Esbuild 的虛擬模塊,可以完成很豐富的功能,除了上述插件實(shí)例中在內(nèi)存中計(jì)算出 env 的值作為模塊內(nèi)容,還可以模塊名當(dāng)做一個(gè)函數(shù)來進(jìn)行編譯,甚至可以在編譯階段實(shí)現(xiàn)函數(shù)遞歸的過程。比如這個(gè) Esbuild 插件:
{name:?'fibo',setup(build)?{build.onResolve({?filter:?/^fib\(\d+\)/?},?args?=>?{return?{?path:?args.path,?namespace:?'fib'?}})build.onLoad({?filter:?/^fib\(\d+\)/,?namespace:?'fib'?},?args?=>?{const?match?=?/^fib\((\d+)\)/.exec(args.path);n?=?Number(match[1]);console.log(n);let?contents?=?n?<?2???`export?default?${n+1}`?:?`import?n1?from?'fib(${n?-?1})'import?n2?from?'fib(${n?-?2})'export?default?n1?+?n2`return?{?contents?}})} }引入這個(gè)插件,可以解析如下的 import 語句:
import?fib5?from?'fib(5)'console.log(fib5)//?13所有的模塊都是虛擬模塊,在真實(shí)文件系統(tǒng)中并不存在
另外,還能借助虛擬模塊來進(jìn)行 URL Import,支持如下的 import 代碼:
import?React?from?'https://esm.sh/react@17'這也可以在插件當(dāng)中實(shí)現(xiàn),可參考示例。
落地場景
1. 代碼壓縮工具
Esbuild 的代碼壓縮功能非常優(yōu)秀,可以甩開傳統(tǒng)的壓縮工具一個(gè)量級(jí)以上的性能差距。Vite 在 2.6 版本也官宣在生產(chǎn)環(huán)境中直接使用 Esbuild 來壓縮 JS 和 CSS 代碼。
2. 代替 ts-node
社區(qū)已經(jīng)有了相應(yīng)的方案 esno: https://github.com/antfu/esno
ts-node?index.ts //?替換為 esno?hello.ts3. 代替 ts-jest
使用 esbuild-jest 代替ts-jest,我曾經(jīng)嘗試在某些大型包中使用 esbuild-jest 來作為 transformer,相比 ts-jest,整體大概提升 3 倍測試效率。
Github 地址:https://github.com/aelbore/esbuild-jest
4. 第三方庫 Bundler
Vite 中在開發(fā)階段使用 Esbuild 來進(jìn)行依賴的預(yù)打包,將所有用到的第三方依賴轉(zhuǎn)成 ESM 格式 Bundle 產(chǎn)物,并且未來有用到生產(chǎn)環(huán)境的打算。
同時(shí)業(yè)界也有一些平臺(tái)基于純 Esbuild 來做線上 cjs -> esm 的 CDN 服務(wù),比如 esm.sh ?和 skypack:
5. 打包 Node 庫
為什么要打包 Node 庫:
減少 node_modules 代碼,避免業(yè)務(wù)安裝一大堆 node_modules 的代碼,減少安裝體積
提高啟動(dòng)速度,所有代碼打到一個(gè)文件,減少了大量的文件 io 操作
更安全。所有代碼打包也是鎖定依賴版本的一種方式,可以避免之前出現(xiàn)的 coa 包導(dǎo)致的大面積 CI 掛掉的問題,可參考云謙的這篇文章。
這方面 Esbuild 的作用跟現(xiàn)在 vercel 團(tuán)隊(duì)出品的 ncc 差不多,但會(huì)對(duì)代碼的寫法有一些限制,無法分析動(dòng)態(tài) require 或者 import 語句含有變量的情況:
6. 小程序編譯
對(duì)于小程序的場景,也可以使用 Esbuild 來代替 Webpack,大大提升編譯速度,對(duì)于 AST 的轉(zhuǎn)換則通過 Esbuild 插件嵌入 SWC 來實(shí)現(xiàn),實(shí)現(xiàn)快速編譯。詳見 132 的分享 esbuild 上生產(chǎn)。
7. Web 構(gòu)建
Web 場景就顯得比較復(fù)雜了,對(duì)于兼容性和周邊工具生態(tài)的要求比較高,比如低瀏覽器語法降級(jí)、CSS 預(yù)編譯器、HMR 等等,如果要用純 Esbuild 來做,還需要補(bǔ)充很多能力。
之前三元同學(xué)基于 Esbuild 實(shí)現(xiàn)了一套 Web 開發(fā)腳手架 ewas,已經(jīng)在 Github 開源,并且已成功落地到我之前的小冊(cè)項(xiàng)目當(dāng)中,相比 create-react-app 啟動(dòng)速度提升了 100 倍以上(30s -> 0.3s)。倉庫地址: https://github.com/sanyuan0704/ewas。
如今 Remix 1.0 正式發(fā)布,底層使用 Esbuild 構(gòu)建,帶來了極致的性能體驗(yàn),成為 Next.js 強(qiáng)有力的競爭對(duì)手。
但總體來說,目前 Esbuild 對(duì)于真實(shí)的 Web 場景還有很多能力不支持,還有一些硬傷,包括語法不支持降級(jí)到ES5,拆包不靈活、不支持 HMR,對(duì)于真正能作為 Webpack 一樣的構(gòu)建工具來講還有很長的路要走。
·················?若川簡介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會(huì)寫一篇年度總結(jié),已經(jīng)寫了7篇,點(diǎn)擊查看年度總結(jié)。
同時(shí),最近組織了源碼共讀活動(dòng),幫助3000+前端人學(xué)會(huì)看源碼。公眾號(hào)愿景:幫助5年內(nèi)前端人走向前列。
識(shí)別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話題
略。分享、收藏、點(diǎn)贊、在看我的文章就是對(duì)我最大的支持~
總結(jié)
以上是生活随笔為你收集整理的前端构建新世代,Esbuild 原来还能这么玩!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图文并茂重新认识下递归
- 下一篇: 前端学习(2967):实现路由跳转的方式