ESBuild & SWC淺談: 新一代構建工具
大廠技術??堅持周更??精選好文
首先, ESBuild & swc是什么?
ESBuild[1]是基于Go語言開發(fā)的JavaScript Bundler, 由Figma前CTO Evan Wallace開發(fā), 并且也被Vite用于開發(fā)環(huán)境的依賴解析和Transform.
SWC[2]則是基于Rust的JavaScript Compiler(其生態(tài)中也包含打包工具spack), 目前為Next.JS/Parcel/Deno等前端圈知名項目使用.
為什么要關注這兩個工具?
因為...

大家可能在日常工作中遇到過, 項目的構建時間隨著項目體積和復雜度逐漸遞增, 有的時候本地編輯一個項目要等上個大幾分鐘(此處@Webpack)

這個是ESBuild官網對于其打包10份three.js的速度對比

SWC則宣稱其比Babel快20倍(四核情況下可以快70倍)

那么ESBuild & SWC是真的有這么快? 還是開發(fā)者的自說自話? 我們通過實驗來檢驗一下, 先看ESBuild
用ESBuild打包一下 #?編譯
>?build-esb
>?esbuild?./src/app.jsx?--bundle?--outfile=out_esb.js?--minify
#?構建產物的大小和構建時間
out_esb.js??27.4kb
??Done?in?13ms
#?運行產物
node?out_esb.js?"">Hello,?world!
用Webpack打包一下 #?編譯
>?build-wp
>?webpack?--mode=production
#?構建產物
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]
#?構建時間
webpack?5.72.0?compiled?successfully?in?1680?ms
npm?run?build-wp??2.79s?user?0.61s?system?84%?cpu?4.033?total
#?運行
node?out_webpack.js??"">Hello,?world!
讓我們先寫一段非常簡單的代碼
import?*?as?React?from?'react'
import?*?as?ReactServer?from?'react-dom/server'
const?Greet?=?()?=>?Hello,?world!
console.log(ReactServer.renderToString()) 然后我們來通過Webpack & ESBuild構建它
再來看看swc的編譯效率
又是一段簡單的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轉譯一下 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轉譯一下 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.兩者的產物對比 //?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
從上面的數據可以看出
在打包代碼的對比, ESBuild的速度(20ms)遠快于Webpack(1680ms) 在編譯代碼的對比, swc也對babel有比較明顯的性能優(yōu)勢(0.63s vs 2.38s). 需要額外說明的是, 用作實例的代碼非常簡單, 并且在對比中也沒有充分使用各個構建工具所有的構建優(yōu)化策略, 只是對比最基礎的配置下幾種工具的速度, 這個和各個工具所羅列的benchmark數據會有差異, 并且構建速度也和硬件性能/運行時狀態(tài)有關.
ESBuild/swc這么快? 那是不是可以直接把Webpack/Babel扔掉了? 也別急, 目前的ESBuild和Swc可能還不能完全替代Webpack. 但是通過這篇分享我們也許可以對它們有一個更全面的認知, 也可以探索后邊在工作中使用這些新一代前端工具的機會
ESBuild/swc在前端生態(tài)中的定位
在當今的前端世界里, 新工具層出不窮, 有的時候不同的工具太多以至于有段時間我完全分不清這些工具各自的功能是什么, 所以我們先來研究一下ESBuild/swc在當今前端工程體系中的角色.

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

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

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

ESBuild/SWC為何這么快?
思考一下, Go & Rust這兩個語言和JavaScript相比有什么差異?
ESBuild的實現(參考ESBuild FAQ[3])
由Go實現并編譯成本地代碼: 多數Bundler都是由JavaScript實現的, 但是CLI應用對于JIT編譯語言來說是性能表現最不好的。每次運行Bundler的時候, JS虛擬機都是以第一次運行代碼的視角來解析Bundler(比如Webpack)的代碼, 沒有優(yōu)化信息. 當ESBuild在解析JavaScript的時候, Node還在解析Bundler的JS代碼
重度使用并行計算: Go語言本身的設計就很重視并行計算, 所以ESBuild對這一點會加以利用. 在構建中主要有三個環(huán)節(jié): 解析(Parsing), 鏈接(Linking)和代碼生成(Code generation), 在解析和代碼生成環(huán)節(jié)會盡可能使用多核進行并行計算
ESBuild 中的一切代碼從零實現: 通過自行實現所有邏輯來避免第三方庫帶來的性能問題, 統一的數據結構可以減少數據轉換開銷, 并且可以根據需要改變架構, 當然最大的缺點就是工作量倍增.
令人想到了SpaceX這家公司, 大量零部件都是自己內部生產, 有效降低生產成本 
對內存的高效使用: ESBuild在實現時盡量減少數據的傳遞以及數據的轉換, ESBuild盡量減少了對整體AST的傳遞, 并且盡可能復用AST數據, 其他的Bundler可能會在編譯的不同階段往復轉換數據格式(string -> TS -> JS -> older JS -> string...). 在內存存儲效率方面Go也比JavaScript更高效.
swc的實現
swc的官方文檔和網站并沒有對swc內部實現的較為具體的解釋, 根據其博客[4]中的一些分析, babel緩慢的主要原因還是來自于其單線程的特性
一點總結
從ESBuild和swc的官方資源中, 共同提到的一點就是利用好并行計算。JS因為在設計之初的目標就是服務好瀏覽器場景, 所以單線程 & 事件驅動并不適合用來進行CPU密集的計算, 而ESBuild/Rust也正是在這一點上對基于Node的構建工具擁有系統性的速度優(yōu)勢。
如何用ESBuild/swc提效?
現在我們知道ESBuild/Rust是做什么的, 并且有什么特點, 我們可以在工作中如何利用ESBuild/swc去改善我們的開發(fā)體驗呢?
使用ESBuild
ESBuild在API層面上非常簡潔, 主要的API只有兩個: Transform和Build, 這兩個API可以通過CLI, JavaScript, Go的方式調用
Transform主要用于對源代碼的轉換, 接受的輸入是字符串, 輸出的是轉換后的代碼 #?用CLI方式調用,?將ts代碼轉化為js代碼
echo?'let?x:?number?=?1'?|?esbuild?--loader=ts?=>?let?x?=?1;Build主要用于構建, 接受的輸入是一個或多個文件 //?用JS模式調用build方法
require('esbuild').buildSync({
??entryPoints:?['in.js'],
??bundle:?true,
??outfile:?'out.js',
})
ESBuild的內容類型(Content Type)包括了ES在打包時可以解析的文件類型, 這一點和Webpack的loader概念類似, 下面的例子是在打包時用JSX Loader解析JS文件.
require('esbuild').buildSync({
??entryPoints:?['app.js'],
??bundle:?true,
??loader:?{?'.js':?'jsx'?},
??outfile:?'out.js',
})
ESBuild也包含插件系統, 可以在構建過程中(Transform API無法使用插件)通過插件更改你的構建流程
//?來自于官網的插件示范
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
如果你覺得目前完全使用ESBuild還不成熟, 也可以在Webpack體系中使用ESBuild的loader來替代babel用于進行代碼轉換, 除此之外, 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'
????????})
??????]
????}
}
注意點
ESBuild不能轉ES5代碼和一些其他語法, 詳情可參考https://esbuild.github.io/content-types/#javascript-caveats
使用Vite
要說2021年前端圈關注度較高的新工具, Vite可以說是名列前茅, 那么Vite和ESBuild/swc有什么關系呢?

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

Vite在開發(fā)環(huán)境使用了ESBuild進行預構建, 在生產環(huán)境使用了Rollup打包, 后續(xù)也有可能使用ESBuild進行生產環(huán)境的構建.

支持ES5需要引入插件 https://github.com/vitejs/vite/tree/main/packages/plugin-legacy
使用swc
Comilation
Transform: 代碼轉換API, 輸入源代碼 => 輸出轉換后的代碼 Parse: 對源代碼進行解析, 輸出AST Minify: 對代碼進行最小化 可以使用swc命令行工具(swc/cli)配合配置文件[6]對文件進行編譯
#?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進行代碼轉換
如果你想在Webpack體系下使用swc(替代babel), 也可以使用swc-loader

Bundle
??swc也支持進行打包功能, 但是目前功能還不很完備, 并且在使用中也有不少Bug. 筆者目前在本地嘗試用spack打包一個簡單的React應用目前還不成功, 還做不到開箱即用 
目前swc的Bundle工具叫spack, 后續(xù)會改名為swcpack. 打包可以通過spack.config.js[7]文件進行配置
一點點總結和思考
全文總結
ESBuild/swc是用編譯型語言編寫的新一代前端工具, 對JS編寫的構建工具有系統級的速度優(yōu)勢
ESBuild可以用于編譯JS代碼和模塊打包, swc號稱也都可以支持兩者但是其打包工具還處于早期開發(fā)階段
目前這兩個工具還不能完全替代Webpack等主流工具這些年發(fā)展出的龐大生態(tài)
當已有的基礎設施穩(wěn)定并且替換成本較大時, 可以嘗試漸進式的利用新工具(loader)或者Vite這種基于ESBuild二次封裝的構建工具
延伸思考
持續(xù)關注前端生態(tài)新發(fā)展, 利用好開源社區(qū)提升研發(fā)效率和體驗的新工具.
在使用新工具的同時, 了解或參與到其背后的技術原理, Go可以作為服務端語言, Rust可以作為系統編程語言, 學習新語言能打開新天地, 豈不美哉?
??感謝收看??
?? 謝謝支持
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了?分享、點贊、收藏?三連哦~。
歡迎關注公眾號 趣談前端?收獲大廠一手好文章~
