Vite 底層采用 雙引擎架構,核心構建引擎是 Esbuild 和 Rollup,二者在開發和生產環境中分工協作,共同實現高性能構建。不可否認,作為 Vite 的雙引擎之一,Esbuild 在很多關鍵的構建階段(如依賴預編譯
、TS 語法轉譯
、代碼壓縮
)讓 Vite 獲得了相當優異的性能,是 Vite 高性能的得力助手。無論是在 Vite 的配置項還是源碼實現中,都包含了不少 Esbuild 本身的基本概念和高階用法。因此,要深入掌握 Vite,學習 Esbuild 必不可少。
本篇文章我將由?Esbuild 開始,講解 Vite 強大的雙引擎結構。強烈建議上手操作,讀兩三遍,不如上手寫一遍 。《esbuild 中文文檔》
🔍 一、為什么?Esbuild 的性能極高?
極速構建:
- 性能碾壓傳統工具:基于 Go 語言編寫,多進程并行處理,比 Webpack/Rollup 快 10-100 倍(10個 three.js 副本打包僅需 0.39秒 vs Webpack 的 41秒) 。
- 無緩存仍高效:內置優化算法,無需依賴緩存即可實現秒級編譯 。
?
開箱即用的支持:
- 語言支持:原生處理 JS、TS、JSX、CSS(含 CSS Modules),無需額外配置 。
- 模塊化:無縫捆綁 ESM 和 CommonJS 模塊,自動樹搖(Tree Shaking) 。
?
多場景適配:
- 瀏覽器環境:默認輸出瀏覽器兼容代碼,支持?
--minify
?壓縮、--sourcemap
?源碼映射 。 - Node 環境:通過?
--platform=node
?打包,剝離 TS 類型、轉換 ESM→CommonJS 。
?🛠? 二、?Esbuild 安裝與使用
npm install esbuild # 或 yarn add esbuild
1. 命令行調用
命令行方式調用也是最簡單的使用方式。我們先來寫一些示例代碼,新建src/index.jsx
文件,內容如下:
import Server from "react-dom/server";let Greet = () => <h1>祝所有高三的同學,金榜題名!</h1>;
console.log(Server.renderToString(<Greet />));
注意安裝一下所需的依賴,在終端執行如下的命令:
npm install react react-dom
接著到package.json
中添加build
腳本:
"scripts": {"build": "esbuild src/index.jsx --bundle --outfile=dist/out.js",},
現在,你可以在終端執行npm run build
,可以發現如下的日志信息:
接著我們就可以看到dish目錄中的打包產物
?
說明我們已經成功通過命令行完成了 Esbuild 打包!但命令行的使用方式不夠靈活,只能傳入一些簡單的命令行參數,稍微復雜的場景就不適用了,所以一般情況下我們還是會用代碼調用的方式。
2. 代碼調用
Esbuild 對外暴露了一系列的 API,主要包括兩類: Build API
和Transform API
,我們可以在 Nodejs 代碼中通過調用這些 API 來使用 Esbuild 的各種功能。想要更全面的了解的,可以去訪問文章開頭的文檔地址。
項目打包——Build API
Build API
主要用來進行項目打包,包括build
、buildSync
和?serve
三個方法。
A、build
?方法:異步打包
功能:執行異步構建任務,返回 Promise 對象,支持插件和并行操作。
適用場景:生產環境打包、復雜構建流程(如代碼分割、壓縮)。
首先我們來試著在 Node.js 中使用build
方法。你可以在項目根目錄新建build.js
文件,內容如下:
import { build } from 'esbuild';async function runBuild() {// 異步方法,返回一個 Promiseconst result = await build({// ---- 如下是一些常見的配置 --- // 當前項目根目錄absWorkingDir: process.cwd(),// 入口文件列表,為一個數組entryPoints: ["./src/index.jsx"],// 打包產物目錄outdir: "dist",// 是否需要打包,一般設為 truebundle: true,// 模塊格式,包括`esm`、`commonjs`和`iife`format: "esm",// 需要排除打包的依賴列表external: [],// 是否開啟自動拆包splitting: true,// 是否生成 SourceMap 文件sourcemap: true,// 是否生成打包的元信息文件metafile: true,// 是否進行代碼壓縮minify: false,// 是否開啟 watch 模式,在 watch 模式下代碼變動則會觸發重新打包watch: false,// 是否將產物寫入磁盤write: true,// Esbuild 內置了一系列的 loader,包括 base64、binary、css、dataurl、file、js(x)、ts(x)、text、json// 針對一些特殊的文件,調用不同的 loader 進行加載loader: {'.png': 'base64',}});console.log(result);
}runBuild();
隨后,你在命令行執行node build.js
,就能在控制臺發現如下日志信息:
接著我們就可以看到dish目錄中的打包產物和相應的 SourceMap 文件
?
B、buildSync
?方法:同步打包 (不推薦)
功能:同步執行構建任務,立即返回結果,但阻塞主線程。
適用場景:小型項目、簡單腳本或 CLI 工具。
一個簡單的例子:
const result = esbuild.buildSync({entryPoints: ['app.js'],bundle: true,outfile: 'out.js',platform: 'node' // 指定 Node 環境
});if (result.errors.length > 0) {throw new Error('Build failed');
}
局限性:
- 性能影響:阻塞主線程,可能導致界面卡頓
- 插件限制:Rollup 等工具的?
buildSync
?不支持插件 - 適用性:僅推薦在輕量任務中使用
難道就不能使用同步打包了嗎??? ? ? ? ??如果說有,其實也是有的
使用?build
?+?await
?實現偽同步:
async function runBuild() {await esbuild.build({ /* 配置 */ });
}
?
C、serve
?方法:開發服務器
這個 API 有 3 個特點。
-
開啟 serve 模式后,將在指定的端口和目錄上搭建一個
靜態文件服務
,這個服務器用原生 Go 語言實現,性能比 Nodejs 更高。 -
類似 webpack-dev-server,所有的產物文件都默認不會寫到磁盤,而是放在內存中,通過請求服務來訪問。
-
每次請求到來時,都會進行重新構建(
rebuild
),永遠返回新的產物。
下面,我們通過一個具體例子來感受一下。
// build.js
import { serve } from 'esbuild';function runBuild() {serve({port: 8000,servedir: './dist',onRequest: (args) => {if (args.path === '/') {args.path = '/index.html'}}},{absWorkingDir: process.cwd(),entryPoints: ["./src/index.jsx"],bundle: true,format: "esm",splitting: true,sourcemap: true,outdir: "dist",loader: {'.js': 'jsx','.png': 'file','.jpg': 'file'}}).then((server) => {console.log("HTTP Server starts at port", server.port);});
}runBuild();
1.運行構建命令
npm run build
2.啟動服務器
node build.js
我們在瀏覽器訪問http://localhost:8000/dist/index.js可以看到 Esbuild 服務器返回的編譯產物如下所示:
后續每次在瀏覽器請求都會觸發 Esbuild 重新構建,而每次重新構建都是一個增量構建的過程,耗時也會比首次構建少很多(一般能減少 70% 左右)。
Serve API 只適合在開發階段使用,不適用于生產環境。
單文件轉譯——Transform API
功能:對單個字符串內容進行轉換(如轉譯 TS/JSX),不訪問文件系統,適用于非文件環境(如瀏覽器內聯處理)或作為工具鏈一環。
與 Build API
類似,它也包含了同步和異步的兩個方法,分別是transformSync
和transform
。
舉例栗子:在項目根目錄新建transform.js
,
// transform.js
import { transform, transformSync } from 'esbuild';async function runTransform() {// 第一個參數是代碼字符串,第二個參數為編譯配置const content = await transform("const isNull = (str: string): boolean => str.length > 0;",{sourcemap: true,loader: "tsx",});console.log(content);
}runTransform();
終端輸入:
node transform.js
接著你就會看見:
同樣的步驟,傳參:
// transform.js
import { transform, transformSync } from 'esbuild';async function runTransform(code = "const isNull = (str: string): boolean => str.length > 0;") {// 第一個參數是代碼字符串,第二個參數為編譯配置const content = await transform(code,{sourcemap: true,loader: "tsx",});console.log(content);
}const inputCode = process.argv[2];
runTransform(inputCode).catch(console.error);
終端輸入:
node transform.js "const add = (a: number, b: number): number => a + b;"
打印出:
由于同步的 API 會使 Esbuild 喪失并發任務處理
的優勢(Build API
的部分已經分析過),我同樣也不推薦大家使用transformSync
。出于性能考慮,Vite 的底層實現也是采用 transform
這個異步的 API 進行 TS 及 JSX 的單文件轉譯的。
📊 三、總結
Esbuild的優勢在于編譯速度非常快,且擁有Go語言的優勢,Go語言編寫的程序比JavaScript少了一個動態解釋的過程;在代碼實現上,Esbuild使用比較克制,很多在Webpack上使用插件實現的功能如loader、minify等均使用Go實現。;劣勢在于支持不完善,提供的功能很基礎,對代碼分割和css處理等支持較弱。
??優勢
- 速度為王:Go 語言 + 并行處理 + 內置功能(減少 AST 轉換鏈) 。
? - 輕量 API:提供 CLI、JS、Go 三種接口,配置簡潔 。
? - 生產優化:默認支持 Tree Shaking、代碼壓縮、Source Map 。
??局限性
1.生態插件較弱
- 不支持 Vue/Sass/Less 等語法,需 JS 插件(性能下降) 。
- 無熱更新(HMR),依賴?
--watch
?或手動重啟 。
2.高級功能缺失
- 無 AST 操作接口,無法實現類 Babel 按需引入 。
- 代碼分割(Code Splitting)對非 ESM 包支持差 。
?
但是不可否認,它的作用和潛力,我相信 Esbuild 未來在持續迭代中, 生態完善后或顛覆前端構建范式。