esbuild 原理与速度革命
esbuild 原理与速度革命
写在前面
上一篇讲了 webpack 的辉煌与瓶颈。这一篇讲 esbuild——它重新定义了"快"的含义,从 webpack 的 60 秒压到 0.5 秒,让前端工程界第一次认真思考:构建工具的天花板可以重写。
本篇会深入 esbuild 的核心设计:为什么 Go 写的编译器能比 webpack 快 100 倍,以及 esbuild 的局限决定了它不会完全替代 webpack。
一、esbuild 是什么
1.1 一句话定义
esbuild 是一个用 Go 语言写的 JavaScript/TypeScript 打包器,主打极致的构建速度。
由 Figma 的 CTO Evan Wallace 一个人业余时间写的(2020 年开源),完全颠覆了构建工具的认知。
1.2 性能对比
同样的项目,各工具的构建时间(越小越好):
esbuild: ████ 0.5s
swc: ████████ 1.5s
parcel: ███████████████ 5s
rollup: ████████████████████████ 10s
webpack: ████████████████████████████████████████████████████████ 60s
100 倍速度差。
二、为什么这么快
esbuild 的快来自多个层面的优化叠加。
2.1 编程语言:Go
JS 是解释型语言(虽然有 V8 JIT),Go 是编译型 + 静态类型 + 垃圾回收:
| 维度 | Node.js (JS) | Go |
|---|---|---|
| 启动时间 | 100ms | 5ms |
| 内存效率 | 一般(对象头 16-24 字节) | 高(对象头 8-16 字节) |
| 并发 | 单线程 + 事件循环 | 多线程 + goroutine |
| 类型 | 动态 | 静态 |
| 性能(计算密集) | 慢 | 快 10-100x |
同样的代码逻辑,Go 比 Node.js 快 10-100 倍(计算密集场景)。
2.2 并行处理
esbuild 把能并行的任务都并行:
// 简化的伪代码
func build() {
var wg sync.WaitGroup
jobs := make(chan string)
// 启动多个 worker
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for file := range jobs {
parse(file) // 每个 worker 处理一个文件
}
}()
}
// 派发所有 JS 文件
for _, file := range allJSFiles {
jobs <- file
}
close(jobs)
wg.Wait()
}
8 核 CPU,理论 8 倍速度。
webpack 是单线程,Node.js 的 worker_threads 用得很少。
2.3 内存中表示所有内容
webpack 在编译过程中,把 AST 转成字符串再解析,反复 parse/unparse。
esbuild 全程在内存中表示 AST:
JS 源码 → Lexer → Token → Parser → AST(内存)
↓
Linker(内存)
↓
Printer → 目标代码
整个过程只 parse 一次,只 generate 一次,中间没有任何字符串转换。
2.4 精心设计的数据结构
// esbuild 的 AST 节点(简化)
type ASTNode struct {
Kind uint8 // 节点类型(用 uint8 而不是 string)
Flags uint16
NumRef uint32
Ref Ref
Data interface{}
}
紧凑的字段布局,缓存友好。
而 webpack 用的 JS 对象:
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: [...],
}
每个对象 100+ 字节,GC 压力大。
2.5 不做的事(同样重要)
esbuild 故意不做这些事情,换速度:
不做 TS 类型检查
// esbuild 不报这个错
function add(a: number, b: string): number {
return a + b; // TS:类型错误,esbuild 静默通过
}
类型检查交给 IDE(tsc –noEmit)。
不打 bundle(默认)
esbuild app.js
# 输出:app.js(单文件,不打包)
要做 bundle 必须显式 --bundle。
不生成 source map(默认)
esbuild app.js --sourcemap
默认关闭,需要时开启。
不做复杂的 chunk 分割
简单的代码分割,不做复杂的 optimization。
不做 CSS Modules / PostCSS 复杂处理
简单 CSS 支持,PostCSS 插件需要 fork。
2.6 性能数据
来看 Evan Wallace 的官方 benchmark:
| 场景 | esbuild | webpack 5 | swc |
|---|---|---|---|
| Three.js 构建 | 0.12s | 22.3s | 1.49s |
| Vue 3 构建 | 0.04s | 6.27s | 0.51s |
| 整个 lodash 构建 | 0.04s | 1.31s | 0.21s |
10-100 倍速度差。
三、esbuild 的 API
3.1 CLI
# 单文件转换
esbuild src/index.ts --outfile=dist/bundle.js
# 打包
esbuild src/index.ts --bundle --outfile=dist/bundle.js
# 监听
esbuild src/index.ts --bundle --watch
# 启用 source map
esbuild src/index.ts --bundle --sourcemap
# 压缩
esbuild src/index.ts --bundle --minify
# 多入口
esbuild src/entry1.ts src/entry2.ts --bundle --outdir=dist
# 平台
esbuild src/index.ts --bundle --platform=browser --format=esm
3.2 JS API
import * as esbuild from 'esbuild';
const result = await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
target: ['es2020'],
platform: 'browser',
format: 'esm',
loader: {
'.png': 'file',
'.svg': 'text',
'.txt': 'text',
},
define: {
'process.env.NODE_ENV': '"production"',
},
});
console.log(result.errors);
console.log(result.warnings);
3.3 监听模式
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
});
await ctx.watch();
// 重新构建
const result = await ctx.rebuild();
// 释放资源
await ctx.dispose();
3.4 Server 模式
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
});
const { host, port } = await ctx.serve({
servedir: 'dist',
port: 3000,
});
console.log(`Server running at http://${host}:${port}`);
四、esbuild 的核心 API 设计
4.1 内置 Loader
| Loader | 用途 |
|---|---|
js |
JavaScript |
jsx |
React JSX |
ts |
TypeScript |
tsx |
TypeScript + JSX |
css |
CSS |
json |
JSON |
text |
转字符串 |
base64 |
转 base64 |
file |
输出文件,返回 URL |
binary |
转 Uint8Array |
dataurl |
转 data URL |
copy |
拷贝到 outdir |
empty |
输出空文件 |
{
loader: {
'.png': 'file',
'.svg': 'dataurl', // SVG 转 base64
'.txt': 'text',
'.woff2': 'file',
},
}
4.2 内置 Plugin 接口
esbuild 的插件 API 比 webpack 简单得多:
const myPlugin = {
name: 'my-plugin',
setup(build) {
// 解析阶段
build.onResolve({ filter: /^virtual:/ }, args => ({
path: args.path,
namespace: 'virtual',
}));
// 加载阶段
build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => ({
contents: `export default ${JSON.stringify(args.path)};`,
}));
},
};
await esbuild.build({
plugins: [myPlugin],
});
4.3 实战插件:解析 .env 文件
const dotenvPlugin = {
name: 'dotenv',
setup(build) {
build.onResolve({ filter: /^\.env$/ }, args => ({
path: args.path,
namespace: 'env',
}));
build.onLoad({ filter: /.*/, namespace: 'env' }, () => {
const env = {};
const fs = require('fs');
const content = fs.readFileSync('.env', 'utf8');
content.split('\n').forEach(line => {
const [k, ...v] = line.split('=');
if (k && v.length) {
env[k.trim()] = v.join('=').trim();
}
});
return {
contents: `export default ${JSON.stringify(env)};`,
};
});
},
};
五、esbuild 的局限
esbuild 不是银弹,有明确的边界。
5.1 不做 TS 类型检查
类型错误不报错,只删类型注解。
// tsc 报错,esbuild 通过
function add(a: number, b: string) {
return a + b;
}
生产项目必须搭配 tsc:tsc --noEmit && esbuild。
5.2 不做复杂的代码分割
简单的 splitChunks,复杂的分包逻辑没有。
5.3 CSS 处理有限
- 不支持 CSS Modules 的
:local - 不支持 SCSS/Less(需要外部预处理)
- PostCSS 插件体系不完整
5.4 不支持 HMR 完整的开发体验
esbuild 提供 serve 模式,但 HMR 支持简单——没有 Vite 那么丝滑。
5.5 插件生态不成熟
相比 webpack 几千个插件,esbuild 的插件生态还在早期。
六、esbuild 在工程中的角色
6.1 作为底层编译器
Vite 用 esbuild 做依赖预打包:
// Vite 用 esbuild 把 CommonJS 转成 ESM
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
optimizeDeps: {
esbuildOptions: {
target: 'es2020',
},
},
});
Vite 启动时用 esbuild 把 node_modules 里的 CommonJS 依赖转成 ESM,加速 dev 启动。
6.2 作为 JS/TS 转译器
很多项目的 CI 流程:
- name: Build
run: |
npx tsc --noEmit # 类型检查
npx esbuild src/index.ts \
--bundle \
--minify \
--sourcemap \
--outfile=dist/bundle.js
6.3 作为小型工具
写个 CLI 工具:
// build.js
import * as esbuild from 'esbuild';
import { readFileSync } from 'fs';
await esbuild.build({
stdin: {
contents: readFileSync('src/index.ts', 'utf8'),
resolveDir: 'src',
sourcefile: 'index.ts',
},
bundle: true,
outfile: 'dist/bundle.js',
});
七、esbuild 之外的 Go 系工具
7.1 swc
swc 是另一款 Rust 写的 JS/TS 转译器(注意,Rust 不是 Go)。
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"target": "es2020"
}
}
- 比 esbuild 慢一点,但生态更成熟
- Next.js、Hugo、Vue 都在用 swc
- swc 的优势:Plugin API 比 esbuild 完整
7.2 Bun
Bun 用 Zig 写,全栈运行时 + 打包器:
- JavaScript 运行时(替代 Node)
- 包管理器(替代 npm)
- 转译器(基于 Zig)
- 测试运行器
八、esbuild 在我们项目的应用
公司项目用 esbuild 做两件事:
8.1 工具链:CLI 打包
// scripts/build.js
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/cli.ts'],
bundle: true,
platform: 'node',
target: 'node18',
outfile: 'dist/cli.js',
external: ['fsevents', 'aws-sdk'],
});
8.2 库代码转译
// scripts/build-lib.js
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'esm',
target: 'es2020',
outfile: 'dist/index.mjs',
sourcemap: true,
});
九、本篇小结
| 优化点 | 效果 |
|---|---|
| Go 语言 | 10-100x 计算速度 |
| 并行处理 | 多核 CPU 充分利用 |
| 内存 AST | 不反复 parse |
| 故意不做的事 | 类型检查、复杂分割、PostCSS |
| 紧凑数据结构 | 缓存友好,GC 压力小 |
| 局限 | 应对 |
|---|---|
| 不做类型检查 | 配合 tsc --noEmit |
| 不做复杂分包 | 用 Vite / Rsbuild |
| CSS 支持有限 | 外部预处理器 |
下一篇讲 Vite 双引擎与 ESM 魔法——开发体验的天花板是怎么做到的。