ESBuild & SWC淺談: 新一代構(gòu)建工具
首先, ESBuild & swc是什么?
- ESBuild[1]是基于Go語(yǔ)言開(kāi)發(fā)的JavaScript Bundler, 由Figma前CTO Evan Wallace開(kāi)發(fā), 并且也被Vite用于開(kāi)發(fā)環(huán)境的依賴解析和Transform.
- SWC[2]則是基于Rust的JavaScript Compiler(其生態(tài)中也包含打包工具spack), 目前為Next.JS/Parcel/Deno等前端圈知名項(xiàng)目使用.
為什么要關(guān)注這兩個(gè)工具?
因?yàn)?..
大家可能在日常工作中遇到過(guò), 項(xiàng)目的構(gòu)建時(shí)間隨著項(xiàng)目體積和復(fù)雜度逐漸遞增, 有的時(shí)候本地編輯一個(gè)項(xiàng)目要等上個(gè)大幾分鐘(此處@Webpack)
這個(gè)是ESBuild官網(wǎng)對(duì)于其打包10份three.js的速度對(duì)比
- SWC則宣稱其比Babel快20倍(四核情況下可以快70倍)

那么ESBuild & SWC是真的有這么快? 還是開(kāi)發(fā)者的自說(shuō)自話? 我們通過(guò)實(shí)驗(yàn)來(lái)檢驗(yàn)一下, 先看ESBuild
- 用ESBuild打包一下
#?編譯
>?build-esb
>?esbuild?./src/app.jsx?--bundle?--outfile=out_esb.js?--minify
#?構(gòu)建產(chǎn)物的大小和構(gòu)建時(shí)間
out_esb.js??27.4kb
??Done?in?13ms
#?運(yùn)行產(chǎn)物
node?out_esb.js?"">Hello,?world!
- 用Webpack打包一下
#?編譯
>?build-wp
>?webpack?--mode=production
#?構(gòu)建產(chǎn)物
asset?out_webpack.js?25.9?KiB?[compared?for?emit]?[minimized]?(name:?main)?1?related?asset
modules?by?path?./node_modules/react/?8.5?KiB
??./node_modules/react/index.js?189?bytes?[built]?[code?generated]
??./node_modules/react/cjs/react.production.min.js?8.32?KiB?[built]?[code?generated]
modules?by?path?./node_modules/react-dom/?28.2?KiB
??./node_modules/react-dom/server.browser.js?227?bytes?[built]?[code?generated]
??./node_modules/react-dom/cjs/react-dom-server.browser.production.min.js?28?KiB?[built]?[code?generated]
./src/app.jsx?254?bytes?[built]?[code?generated]
./node_modules/object-assign/index.js?2.17?KiB?[built]?[code?generated]
#?構(gòu)建時(shí)間
webpack?5.72.0?compiled?successfully?in?1680?ms
npm?run?build-wp??2.79s?user?0.61s?system?84%?cpu?4.033?total
#?運(yùn)行
node?out_webpack.js??"">Hello,?world!
讓我們先寫(xiě)一段非常簡(jiǎn)單的代碼
import?*?as?React?from?'react'
import?*?as?ReactServer?from?'react-dom/server'
const?Greet?=?()?=>?Hello,?world!
console.log(ReactServer.renderToString()) 然后我們來(lái)通過(guò)Webpack & ESBuild構(gòu)建它
- 用ESBuild打包一下
再來(lái)看看swc的編譯效率
- 又是一段簡(jiǎn)單的ES6代碼
//?一些變量聲明
const?PI?=?3.1415;
let?x?=?1;
//?spread
let?[foo,?[[bar],?baz]]?=?[1,?[[2],?3]];
const?node?=?{
??loc:?{
????start:?{
??????line:?1,
??????column:?5
????}
??}
};
let?{?loc,?loc:?{?start?},?loc:?{?start:?{?line?}}?}?=?node;
//?arrow?function
var?sum?=?(num1,?num2)?=>?{?return?num1?+?num2;?}
//?set
const?s?=?new?Set();
[2,?3,?5,?4,?5,?2,?2].forEach(x?=>?s.add(x));
//?class
class?Point?{
??constructor(x,?y)?{
????this.x?=?x;
????this.y?=?y;
??}
??toString()?{
????return?'('?+?this.x?+?',?'?+?this.y?+?')';
??}
} - 先用Babel轉(zhuǎn)譯一下
yarn?compile-babel
yarn?run?v1.16.0
warning?package.json:?No?license?field
$?babel?src/es6.js?-o?es6_babel.js
???Done?in?2.38s. - 再用swc轉(zhuǎn)譯一下
yarn?compile-swc??
yarn?run?v1.16.0
warning?package.json:?No?license?field
$?swc?src/es6.js?-o?es6_swc.js
Successfully?compiled?1?file?with?swc.
???Done?in?0.63s. - 兩者的產(chǎn)物對(duì)比
//?es6_babel
"use?strict";
function?_classCallCheck(instance,?Constructor)?{?if?(!(instance?instanceof?Constructor))?{?throw?new?TypeError("Cannot?call?a?class?as?a?function");?}?}
function?_defineProperties(target,?props)?{?for?(var?i?=?0;?i?false;?descriptor.configurable?=?true;?if?("value"?in?descriptor)?descriptor.writable?=?true;?Object.defineProperty(target,?descriptor.key,?descriptor);?}?}
function?_createClass(Constructor,?protoProps,?staticProps)?{?if?(protoProps)?_defineProperties(Constructor.prototype,?protoProps);?if?(staticProps)?_defineProperties(Constructor,?staticProps);?Object.defineProperty(Constructor,?"prototype",?{?writable:?false?});?return?Constructor;?}
var?PI?=?3.1415;
var?x?=?1;
var?foo?=?1,
????bar?=?2,
????baz?=?3;
var?node?=?{
??loc:?{
????start:?{
??????line:?1,
??????column:?5
????}
??}
};
var?loc?=?node.loc,
????start?=?node.loc.start,
????line?=?node.loc.start.line;
var?sum?=?function?sum(num1,?num2)?{
??return?num1?+?num2;
};
var?s?=?new?Set();
[2,?3,?5,?4,?5,?2,?2].forEach(function?(x)?{
??return?s.add(x);
});
var?Point?=?/*#__PURE__*/function?()?{
??function?Point(x,?y)?{
????_classCallCheck(this,?Point);
????this.x?=?x;
????this.y?=?y;
??}
??_createClass(Point,?[{
????key:?"toString",
????value:?function?toString()?{
??????return?'('?+?this.x?+?',?'?+?this.y?+?')';
????}
??}]);
??return?Point;
}();
//?es6?swc
function?_classCallCheck(instance,?Constructor)?{
????if?(!(instance?instanceof?Constructor))?{
????????throw?new?TypeError("Cannot?call?a?class?as?a?function");
????}
}
function?_defineProperties(target,?props)?{
????for(var?i?=?0;?i?????????var?descriptor?=?props[i];
????????descriptor.enumerable?=?descriptor.enumerable?||?false;
????????descriptor.configurable?=?true;
????????if?("value"?in?descriptor)?descriptor.writable?=?true;
????????Object.defineProperty(target,?descriptor.key,?descriptor);
????}
}
function?_createClass(Constructor,?protoProps,?staticProps)?{
????if?(protoProps)?_defineProperties(Constructor.prototype,?protoProps);
????if?(staticProps)?_defineProperties(Constructor,?staticProps);
????return?Constructor;
}
var?PI?=?3.1415;
var?x?=?1;
var?foo?=?1,?bar?=?2,?baz?=?3;
var?node?=?{
????loc:?{
????????start:?{
????????????line:?1,
????????????column:?5
????????}
????}
};
var?loc?=?node.loc,?start?=?node.loc.start,?_loc?=?node.loc,?line?=?_loc.start.line;
var?sum?=?function(num1,?num2)?{
????return?num1?+?num2;
};
var?s?=?new?Set();
[
????2,
????3,
????5,
????4,
????5,
????2,
????2
].forEach(function(x1)?{
????return?s.add(x1);
});
var?Point?=?/*#__PURE__*/?function()?{
????"use?strict";
????function?Point(x2,?y)?{
????????_classCallCheck(this,?Point);
????????this.x?=?x2;
????????this.y?=?y;
????}
????_createClass(Point,?[
????????{
????????????key:?"toString",
????????????value:?function?toString()?{
????????????????return?"("?+?this.x?+?",?"?+?this.y?+?")";
????????????}
????????}
????]);
????return?Point;
}();
//#?sourceMappingURL=es6_swc.js.map
- 又是一段簡(jiǎn)單的ES6代碼
從上面的數(shù)據(jù)可以看出
- 在打包代碼的對(duì)比, ESBuild的速度(20ms)遠(yuǎn)快于Webpack(1680ms)
- 在編譯代碼的對(duì)比, swc也對(duì)babel有比較明顯的性能優(yōu)勢(shì)(0.63s vs 2.38s).
- 需要額外說(shuō)明的是, 用作實(shí)例的代碼非常簡(jiǎn)單, 并且在對(duì)比中也沒(méi)有充分使用各個(gè)構(gòu)建工具所有的構(gòu)建優(yōu)化策略, 只是對(duì)比最基礎(chǔ)的配置下幾種工具的速度, 這個(gè)和各個(gè)工具所羅列的benchmark數(shù)據(jù)會(huì)有差異, 并且構(gòu)建速度也和硬件性能/運(yùn)行時(shí)狀態(tài)有關(guān).
- ESBuild/swc這么快? 那是不是可以直接把Webpack/Babel扔掉了? 也別急, 目前的ESBuild和Swc可能還不能完全替代Webpack. 但是通過(guò)這篇分享我們也許可以對(duì)它們有一個(gè)更全面的認(rèn)知, 也可以探索后邊在工作中使用這些新一代前端工具的機(jī)會(huì)
ESBuild/swc在前端生態(tài)中的定位
- 在當(dāng)今的前端世界里, 新工具層出不窮, 有的時(shí)候不同的工具太多以至于有段時(shí)間我完全分不清這些工具各自的功能是什么, 所以我們先來(lái)研究一下ESBuild/swc在當(dāng)今前端工程體系中的角色.

從上面的截圖中選擇幾個(gè)我們?nèi)粘=佑|最頻繁的前端工程化工具:
- Loader: 因?yàn)榍岸隧?xiàng)目中包含各種文件類型和數(shù)據(jù), 需要將其進(jìn)行相應(yīng)的轉(zhuǎn)換變成JS模塊才能為打包工具使用并進(jìn)行構(gòu)建. JS的Compiler和其他類型文件的Loader可以統(tǒng)稱為Transfomer.
- Plugin: 可以更一步定制化構(gòu)建流程, 對(duì)模塊進(jìn)行改造(比如壓縮JS的Terser)
- 還有一些前端構(gòu)建工具是基于通用構(gòu)建工具進(jìn)行了一定封裝或者增加額外功能的, 比如CRA/Jupiter/Vite/Umi
Task Runner 任務(wù)運(yùn)行器: 開(kāi)發(fā)者設(shè)置腳本讓構(gòu)建工具完成開(kāi)發(fā)、構(gòu)建、部署中的一系列任務(wù), 大家日常常用的是npm/yarn的腳本功能; 在更早一些時(shí)候, 比較流行Gulp/Grunt這樣的工具
Package Manager 包管理器: 這個(gè)大家都不會(huì)陌生, npm/Yarn/pnmp幫開(kāi)發(fā)者下載并管理好依賴, 對(duì)于現(xiàn)在的前端開(kāi)發(fā)來(lái)說(shuō)必不可少.
Compiler/Transpiler 編譯器: 在市場(chǎng)上很多瀏覽器還只支持ES5語(yǔ)法的時(shí)候, Babel這樣的Comipler在前端開(kāi)發(fā)中必不可少; 如果你是用TypeScript的話, 也需要通過(guò)tsc或者ts-loader進(jìn)行編譯.
Bundler 打包工具: 從開(kāi)發(fā)者設(shè)置的入口出發(fā), 分析模塊依賴, 加載并將各類資源最終打包成1個(gè)或多個(gè)文件的工具.

- ESBuild的定位是Bundler, 但是它也是Compiler(有Transform代碼的能力)

- swc自稱其定位為Compiler + Bundler, 但是目前spack還不是很好用

ESBuild/SWC為何這么快?
- 思考一下, Go & Rust這兩個(gè)語(yǔ)言和JavaScript相比有什么差異?
ESBuild的實(shí)現(xiàn)(參考ESBuild FAQ[3])
- 由Go實(shí)現(xiàn)并編譯成本地代碼: 多數(shù)Bundler都是由JavaScript實(shí)現(xiàn)的, 但是CLI應(yīng)用對(duì)于JIT編譯語(yǔ)言來(lái)說(shuō)是性能表現(xiàn)最不好的。每次運(yùn)行Bundler的時(shí)候, JS虛擬機(jī)都是以第一次運(yùn)行代碼的視角來(lái)解析Bundler(比如Webpack)的代碼, 沒(méi)有優(yōu)化信息. 當(dāng)ESBuild在解析JavaScript的時(shí)候, Node還在解析Bundler的JS代碼
- 重度使用并行計(jì)算: Go語(yǔ)言本身的設(shè)計(jì)就很重視并行計(jì)算, 所以ESBuild對(duì)這一點(diǎn)會(huì)加以利用. 在構(gòu)建中主要有三個(gè)環(huán)節(jié): 解析(Parsing), 鏈接(Linking)和代碼生成(Code generation), 在解析和代碼生成環(huán)節(jié)會(huì)盡可能使用多核進(jìn)行并行計(jì)算
ESBuild 中的一切代碼從零實(shí)現(xiàn): 通過(guò)自行實(shí)現(xiàn)所有邏輯來(lái)避免第三方庫(kù)帶來(lái)的性能問(wèn)題, 統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)可以減少數(shù)據(jù)轉(zhuǎn)換開(kāi)銷, 并且可以根據(jù)需要改變架構(gòu), 當(dāng)然最大的缺點(diǎn)就是工作量倍增.
- 令人想到了SpaceX這家公司, 大量零部件都是自己內(nèi)部生產(chǎn), 有效降低生產(chǎn)成本

- 令人想到了SpaceX這家公司, 大量零部件都是自己內(nèi)部生產(chǎn), 有效降低生產(chǎn)成本
- 對(duì)內(nèi)存的高效使用: ESBuild在實(shí)現(xiàn)時(shí)盡量減少數(shù)據(jù)的傳遞以及數(shù)據(jù)的轉(zhuǎn)換, ESBuild盡量減少了對(duì)整體AST的傳遞, 并且盡可能復(fù)用AST數(shù)據(jù), 其他的Bundler可能會(huì)在編譯的不同階段往復(fù)轉(zhuǎn)換數(shù)據(jù)格式(string -> TS -> JS -> older JS -> string...). 在內(nèi)存存儲(chǔ)效率方面Go也比JavaScript更高效.
swc的實(shí)現(xiàn)
- swc的官方文檔和網(wǎng)站并沒(méi)有對(duì)swc內(nèi)部實(shí)現(xiàn)的較為具體的解釋, 根據(jù)其博客[4]中的一些分析, babel緩慢的主要原因還是來(lái)自于其單線程的特性
一點(diǎn)總結(jié)
- 從ESBuild和swc的官方資源中, 共同提到的一點(diǎn)就是利用好并行計(jì)算。JS因?yàn)樵谠O(shè)計(jì)之初的目標(biāo)就是服務(wù)好瀏覽器場(chǎng)景, 所以單線程 & 事件驅(qū)動(dòng)并不適合用來(lái)進(jìn)行CPU密集的計(jì)算, 而ESBuild/Rust也正是在這一點(diǎn)上對(duì)基于Node的構(gòu)建工具擁有系統(tǒng)性的速度優(yōu)勢(shì)。
如何用ESBuild/swc提效?
- 現(xiàn)在我們知道ESBuild/Rust是做什么的, 并且有什么特點(diǎn), 我們可以在工作中如何利用ESBuild/swc去改善我們的開(kāi)發(fā)體驗(yàn)?zāi)?
使用ESBuild
ESBuild在API層面上非常簡(jiǎn)潔, 主要的API只有兩個(gè): Transform和Build, 這兩個(gè)API可以通過(guò)CLI, JavaScript, Go的方式調(diào)用
- Transform主要用于對(duì)源代碼的轉(zhuǎn)換, 接受的輸入是字符串, 輸出的是轉(zhuǎn)換后的代碼
#?用CLI方式調(diào)用,?將ts代碼轉(zhuǎn)化為js代碼
echo?'let?x:?number?=?1'?|?esbuild?--loader=ts?=>?let?x?=?1; - Build主要用于構(gòu)建, 接受的輸入是一個(gè)或多個(gè)文件
//?用JS模式調(diào)用build方法
require('esbuild').buildSync({
??entryPoints:?['in.js'],
??bundle:?true,
??outfile:?'out.js',
})
- Transform主要用于對(duì)源代碼的轉(zhuǎn)換, 接受的輸入是字符串, 輸出的是轉(zhuǎn)換后的代碼
- ESBuild的內(nèi)容類型(Content Type)包括了ES在打包時(shí)可以解析的文件類型, 這一點(diǎn)和Webpack的loader概念類似, 下面的例子是在打包時(shí)用JSX Loader解析JS文件.
require('esbuild').buildSync({
??entryPoints:?['app.js'],
??bundle:?true,
??loader:?{?'.js':?'jsx'?},
??outfile:?'out.js',
})
- ESBuild也包含插件系統(tǒng), 可以在構(gòu)建過(guò)程中(Transform API無(wú)法使用插件)通過(guò)插件更改你的構(gòu)建流程
//?來(lái)自于官網(wǎng)的插件示范
let?envPlugin?=?{
??name:?'env',
??setup(build)?{
????//?Intercept?import?paths?called?"env"?so?esbuild?doesn't?attempt
????//?to?map?them?to?a?file?system?location.?Tag?them?with?the?"env-ns"
????//?namespace?to?reserve?them?for?this?plugin.
????build.onResolve({?filter:?/^env$/?},?args?=>?({
??????path:?args.path,
??????namespace:?'env-ns',
????}))
????//?Load?paths?tagged?with?the?"env-ns"?namespace?and?behave?as?if
????//?they?point?to?a?JSON?file?containing?the?environment?variables.
????build.onLoad({?filter:?/.*/,?namespace:?'env-ns'?},?()?=>?({
??????contents:?JSON.stringify(process.env),
??????loader:?'json',
????}))
??},
}
//?使用插件
require('esbuild').build({
??entryPoints:?['app.js'],
??bundle:?true,
??outfile:?'out.js',
??plugins:?[envPlugin],
}).catch(()?=>?process.exit(1))
在其他工具中使用ESBuild
- 如果你覺(jué)得目前完全使用ESBuild還不成熟, 也可以在Webpack體系中使用ESBuild的loader來(lái)替代babel用于進(jìn)行代碼轉(zhuǎn)換, 除此之外, esbuild-loader[5]還可以用于JS & CSS的代碼最小化.
const?{?ESBuildMinifyPlugin?}?=?require('esbuild-loader')
module.exports?=?{
????rules:?[
??????{
????????test:?/.js$/,
????????//?使用esbuild作為js/ts/jsx/tsx?loader
????????loader:?'esbuild-loader',
????????options:?{
??????????loader:?'jsx',??
??????????target:?'es2015'
????????}
??????},
????],
????//?或者使用esbuild-loader作為JS壓縮工具
????optimization:?{
??????minimizer:?[
????????new?ESBuildMinifyPlugin({
??????????target:?'es2015'
????????})
??????]
????}
}
- 如果你覺(jué)得目前完全使用ESBuild還不成熟, 也可以在Webpack體系中使用ESBuild的loader來(lái)替代babel用于進(jìn)行代碼轉(zhuǎn)換, 除此之外, esbuild-loader[5]還可以用于JS & CSS的代碼最小化.
注意點(diǎn)
- ESBuild不能轉(zhuǎn)ES5代碼和一些其他語(yǔ)法, 詳情可參考https://esbuild.github.io/content-types/#javascript-caveats
使用Vite
- 要說(shuō)2021年前端圈關(guān)注度較高的新工具, Vite可以說(shuō)是名列前茅, 那么Vite和ESBuild/swc有什么關(guān)系呢?

- Vite的核心理念是使用ESM+編譯語(yǔ)言工具(ESBuild)加快本地運(yùn)行

- Vite在開(kāi)發(fā)環(huán)境使用了ESBuild進(jìn)行預(yù)構(gòu)建, 在生產(chǎn)環(huán)境使用了Rollup打包, 后續(xù)也有可能使用ESBuild進(jìn)行生產(chǎn)環(huán)境的構(gòu)建.

- 支持ES5需要引入插件 https://github.com/vitejs/vite/tree/main/packages/plugin-legacy
使用swc
Comilation
- Transform: 代碼轉(zhuǎn)換API, 輸入源代碼 => 輸出轉(zhuǎn)換后的代碼
- Parse: 對(duì)源代碼進(jìn)行解析, 輸出AST
- Minify: 對(duì)代碼進(jìn)行最小化
可以使用swc命令行工具(swc/cli)配合配置文件[6]對(duì)文件進(jìn)行編譯
#?Transpile?one?file?and?emit?to?stdout
npx?swc?./file.js
#?Transpile?one?file?and?emit?to?`output.js`
npx?swc?./file.js?-o?output.js
#?Transpile?and?write?to?/output?dir
npx?swc?./my-dir?-d?outputswc的核心部分swc/core主要有三種API
swc也推出了swc/wasm模塊, 可以讓用戶在瀏覽器環(huán)境使用wasm進(jìn)行代碼轉(zhuǎn)換
如果你想在Webpack體系下使用swc(替代babel), 也可以使用swc-loader

Bundle
- ??swc也支持進(jìn)行打包功能, 但是目前功能還不很完備, 并且在使用中也有不少Bug. 筆者目前在本地嘗試用spack打包一個(gè)簡(jiǎn)單的React應(yīng)用目前還不成功, 還做不到開(kāi)箱即用

- 目前swc的Bundle工具叫spack, 后續(xù)會(huì)改名為swcpack.
- 打包可以通過(guò)spack.config.js[7]文件進(jìn)行配置
一點(diǎn)點(diǎn)總結(jié)和思考
全文總結(jié)
- ESBuild/swc是用編譯型語(yǔ)言編寫(xiě)的新一代前端工具, 對(duì)JS編寫(xiě)的構(gòu)建工具有系統(tǒng)級(jí)的速度優(yōu)勢(shì)
- ESBuild可以用于編譯JS代碼和模塊打包, swc號(hào)稱也都可以支持兩者但是其打包工具還處于早期開(kāi)發(fā)階段
- 目前這兩個(gè)工具還不能完全替代Webpack等主流工具這些年發(fā)展出的龐大生態(tài)
- 當(dāng)已有的基礎(chǔ)設(shè)施穩(wěn)定并且替換成本較大時(shí), 可以嘗試漸進(jìn)式的利用新工具(loader)或者Vite這種基于ESBuild二次封裝的構(gòu)建工具
延伸思考
- 持續(xù)關(guān)注前端生態(tài)新發(fā)展, 利用好開(kāi)源社區(qū)提升研發(fā)效率和體驗(yàn)的新工具.
- 在使用新工具的同時(shí), 了解或參與到其背后的技術(shù)原理, Go可以作為服務(wù)端語(yǔ)言, Rust可以作為系統(tǒng)編程語(yǔ)言, 學(xué)習(xí)新語(yǔ)言能打開(kāi)新天地, 豈不美哉?
??感謝收看??

參考資料
- ESBuild https://esbuild.github.io/
- SWC https://swc.rs/
- Vite https://cn.vitejs.dev/
- https://blog.logrocket.com/using-spack-bundler-in-rust-to-speed-up-builds/
- https://datastation.multiprocess.io/blog/2021-11-13-benchmarking-esbuild-swc-typescript-babel.html
- https://blog.logrocket.com/webpack-or-esbuild-why-not-both/
參考資料
[1]ESBuild: https://esbuild.github.io/
[2]SWC: https://swc.rs/
[3]FAQ: https://esbuild.github.io/faq/
[4]博客: https://swc.rs/blog/perf-swc-vs-babel
[5]esbuild-loader: https://github.com/privatenumber/esbuild-loader
[6]配置文件: https://swc.rs/docs/configuration/swcrc
[7]spack.config.js: https://swc.rs/docs/configuration/bundling



