ESBuild - SWC 淺談: 新一代構建工具
首先, ESBuild & swc 是什麼?
-
ESBuild[1] 是基於 Go 語言開發的 JavaScript Bundler, 由 Figma 前 CTO Evan Wallace 開發, 並且也被 Vite 用於開發環境的依賴解析和 Transform.
-
SWC[2] 則是基於 Rust 的 JavaScript Compiler(其生態中也包含打包工具 spack), 目前爲 Next.JS/Parcel/Deno 等前端圈知名項目使用.
爲什麼要關注這兩個工具?
-
因爲...
-
-
大家可能在日常工作中遇到過, 項目的構建時間隨着項目體積和複雜度逐漸遞增, 有的時候本地編輯一個項目要等上個大幾分鐘 (此處 @Webpack)
-
-
這個是 ESBuild 官網對於其打包 10 份 three.js 的速度對比
-
-
SWC 則宣稱其比 Babel 快 20 倍 (四核情況下可以快 70 倍)
-
那麼 ESBuild & SWC 是真的有這麼快? 還是開發者的自說自話? 我們通過實驗來檢驗一下, 先看 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 <h1 data-reactroot="">Hello, world!</h1>
-
用 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 <h1 data-reactroot="">Hello, world!</h1>
-
讓我們先寫一段非常簡單的代碼
import * as React from 'react' import * as ReactServer from 'react-dom/server' const Greet = () => <h1>Hello, world!</h1> console.log(ReactServer.renderToString(<Greet />))
-
然後我們來通過 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 < props.length; 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); 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 < props.length; 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 有比較明顯的性能優勢 (0.63s vs 2.38s).
-
需要額外說明的是, 用作實例的代碼非常簡單, 並且在對比中也沒有充分使用各個構建工具所有的構建優化策略, 只是對比最基礎的配置下幾種工具的速度, 這個和各個工具所羅列的 benchmark 數據會有差異, 並且構建速度也和硬件性能 / 運行時狀態有關.
-
ESBuild/swc 這麼快? 那是不是可以直接把 Webpack/Babel 扔掉了? 也別急, 目前的 ESBuild 和 Swc 可能還不能完全替代 Webpack. 但是通過這篇分享我們也許可以對它們有一個更全面的認知, 也可以探索後邊在工作中使用這些新一代前端工具的機會
ESBuild/swc 在前端生態中的定位
- 在當今的前端世界裏, 新工具層出不窮, 有的時候不同的工具太多以至於有段時間我完全分不清這些工具各自的功能是什麼, 所以我們先來研究一下 ESBuild/swc 在當今前端工程體系中的角色.
-
從上面的截圖中選擇幾個我們日常接觸最頻繁的前端工程化工具:
-
Loader: 因爲前端項目中包含各種文件類型和數據, 需要將其進行相應的轉換變成 JS 模塊才能爲打包工具使用並進行構建. JS 的 Compiler 和其他類型文件的 Loader 可以統稱爲 Transfomer.
-
Plugin: 可以更一步定製化構建流程, 對模塊進行改造 (比如壓縮 JS 的 Terser)
-
還有一些前端構建工具是基於通用構建工具進行了一定封裝或者增加額外功能的, 比如 CRA/Jupiter/Vite/Umi
-
Task Runner 任務運行器: 開發者設置腳本讓構建工具完成開發、構建、部署中的一系列任務, 大家日常常用的是 npm/yarn 的腳本功能; 在更早一些時候, 比較流行 Gulp/Grunt 這樣的工具
-
Package Manager 包管理器: 這個大家都不會陌生, npm/Yarn/pnmp 幫開發者下載並管理好依賴, 對於現在的前端開發來說必不可少.
-
Compiler/Transpiler 編譯器: 在市場上很多瀏覽器還只支持 ES5 語法的時候, Babel 這樣的 Comipler 在前端開發中必不可少; 如果你是用 TypeScript 的話, 也需要通過 tsc 或者 ts-loader 進行編譯.
-
Bundler 打包工具: 從開發者設置的入口出發, 分析模塊依賴, 加載並將各類資源最終打包成 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) 的代碼, 沒有優化信息. 當 ESBuild 在解析 JavaScript 的時候, Node 還在解析 Bundler 的 JS 代碼
-
重度使用並行計算: Go 語言本身的設計就很重視並行計算, 所以 ESBuild 對這一點會加以利用. 在構建中主要有三個環節: 解析 (Parsing), 鏈接(Linking) 和代碼生成(Code generation), 在解析和代碼生成環節會盡可能使用多核進行並行計算
-
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 的構建工具擁有系統性的速度優勢。
如何用 ESBuild/swc 提效?
- 現在我們知道 ESBuild/Rust 是做什麼的, 並且有什麼特點, 我們可以在工作中如何利用 ESBuild/swc 去改善我們的開發體驗呢?
使用 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 在開發環境使用了 ESBuild 進行預構建, 在生產環境使用了 Rollup 打包, 後續也有可能使用 ESBuild 進行生產環境的構建.
- 支持 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 output
-
swc 的核心部分 swc/core 主要有三種 API
-
swc 也推出了 swc/wasm 模塊, 可以讓用戶在瀏覽器環境使用 wasm 進行代碼轉換
-
如果你想在 Webpack 體系下使用 swc(替代 babel), 也可以使用 swc-loader
-
Bundle
-
⚠️swc 也支持進行打包功能, 但是目前功能還不很完備, 並且在使用中也有不少 Bug. 筆者目前在本地嘗試用 spack 打包一個簡單的 React 應用目前還不成功, 還做不到開箱即用
-
-
目前 swc 的 Bundle 工具叫 spack, 後續會改名爲 swcpack.
-
打包可以通過 spack.config.js[7] 文件進行配置
一點點總結和思考
全文總結
-
ESBuild/swc 是用編譯型語言編寫的新一代前端工具, 對 JS 編寫的構建工具有系統級的速度優勢
-
ESBuild 可以用於編譯 JS 代碼和模塊打包, swc 號稱也都可以支持兩者但是其打包工具還處於早期開發階段
-
目前這兩個工具還不能完全替代 Webpack 等主流工具這些年發展出的龐大生態
-
當已有的基礎設施穩定並且替換成本較大時, 可以嘗試漸進式的利用新工具 (loader) 或者 Vite 這種基於 ESBuild 二次封裝的構建工具
延伸思考
-
持續關注前端生態新發展, 利用好開源社區提升研發效率和體驗的新工具.
-
在使用新工具的同時, 瞭解或參與到其背後的技術原理, Go 可以作爲服務端語言, Rust 可以作爲系統編程語言, 學習新語言能打開新天地, 豈不美哉?
❤️感謝收看❤️
參考資料
-
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9VaUq9FOm2_nKNCGaH-7rw