概述
- 作为 Vite 的双引擎之一,Esbuild 在很多关键的构建阶段(如 依赖预编译 、 TS 语法转译 、 代码压缩 ) 让 Vite 获得了相当优异的性能,是 Vite 高性能的得力助手
- 无论是在 Vite 的配置项还是源码实现中,都包含了不少 Esbuild 本身的基本概念和高阶用法
高性能的Esbuild
- Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具,相比传统的打包工具,主打性能优势,在构建速度上可以比传统工具快 10~100 倍。获得这样超高的构建性能,主要原因可以概括为 4 点
1 )使用 Golang 开发
- 构建逻辑代码直接被编译为原生机器码,而不用像 JS 一样先代码解析为字节码,然后转换为机器码,大大节省了程序运行时间。
2 ) 多核并行
- 内部打包算法充分利用多核 CPU 优势,所有的步骤尽可能并行,这也是
得益于 Go 当中多线程共享内存的优势。
3 )从零造轮子
- 几乎没有使用任何第三方库,所有逻辑自己编写,大到 AST 解析,小
到字符串的操作,保证极致的代码性能
4 )高效的内存利用
- Esbuild 中从头到尾尽可能地复用一份 AST 节点数据,而不用像 JS 打包工具中频繁地解析和传递 AST 数据(如 string -> TS -> JS -> string),造成内存的大量浪费
使用
- 执行 pnpm init -y 新建一个项目, 然后通过如下的命令完成 Esbuild 的安装:
- $
pnpm i esbuild
- $
- 使用 Esbuild 有 2 种方式,分别是 命令行调用和代码调用
1 )命令行调用
-
命令行方式调用也是最简单的使用方式。我们先来写一些示例代码,新建 src/index.jsx 文件,内容如下
// src/index.jsx import Server from "react-dom/server"; let Greet = () => <h1>Hello, juejin!</h1>; console.log(Server.renderToString(<Greet />));
-
注意安装一下所需的依赖,在终端执行如下的命令:
- $
pnpm install react react-dom
- $
-
接着到 package.json 中添加 build 脚本:
"scripts": { "build": "./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=dist/out.js" },
-
现在,你可以在终端执行 $
pnpm run build
,可以发现如下的日志信息 -
说明我们已经成功通过命令行完成了 Esbuild 打包!但命令行的使用方式不够灵活,只能传入一些简单的命令行参数,稍微复杂的场景就不适用了,所以一般情况下我们还是会用代码调用的方式
2 ) 代码调用
- Esbuild 对外暴露了一系列的 API,主要包括两类: Build API 和 Transform API ,我们可以在 Nodejs 代码中通过调用这些 API 来使用 Esbuild 的各种功能
项目打包——Build API
- Build API 主要用来进行项目打包,包括 build 、 buildSync 和 serve 三个方法。
首先我们来试着在 Node.js 中使用 build 方法。你可以在项目根目录新建 build.js 文
件,内容如下:
const {
build, buildSync, serve } = require("esbuild");
async function runBuild() {
// 异步方法,返回一个 Promise
const result = await build({
// ---- 如下是一些常见的配置 ---
// 当前项目根目录
absWorkingDir: process.cwd(),
// 入口文件列表,为一个数组
entryPoints: ["./src/index.jsx"],
// 打包产物目录
outdir: "dist",
// 是否需要打包,一般设为 true
bundle: 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
// 针对一些特殊的文件,调用不同的 loader 进行加载
loader: {
'.png': 'base64',
}
});
console.log(result);
}
runBuild();
- 随后,你在命令行执行 node build.js ,就能在控制台发现如下日志信息:

-
以上就是 Esbuild 打包的元信息,这对我们编写插件扩展 Esbuild 能力非常有用。
-
接着,我们再观察一下 dist 目录,发现打包产物和相应的 SourceMap 文件也已经成功写入磁盘:

-
其实 buildSync 方法的使用几乎相同,如下代码所示:
function runBuild() { // 同步方法 const result = buildSync({ // 省略一系列的配置 }); console.log(result); } runBuild();
-
但我并不推荐大家使用 buildSync 这种同步的 API,它们会导致两方面不良后果。一方面容易使 Esbuild 在当前线程阻塞,丧失 并发任务处理 的优势。另一方面,Esbuild 所有插件中都不能使用任何异步操作,这给 插件开发 增加了限制
-
因此更推荐使用 build 这个异步 API,它可以很好地避免上述问题。在项目打包方面,除了 build 和 buildSync ,Esbuild 还提供了另外一个比较强大的 API
—— serve 。这个 API 有 3 个特点- 开启 serve 模式后,将在指定的端口和目录上搭建一个静态文件服务 ,这个服务器用原生 Go 语言实现,性能比 Nodejs 更高
- 类似 webpack-dev-server,所有的产物文件都默认不会写到磁盘,而是放在内存中,通过请求服务来访问
- 每次请求到来时,都会进行重新构建( rebuild ),永远返回新的产物
-
值得注意的是,触发 rebuild 的条件并不是代码改动,而是新的请求到来
-
现在,举一个例子
// build.js const { build, buildSync, serve } = require("esbuild"); function runBuild() { serve({ port: 8000, // 静态资源目录 servedir: './dist' }, { absWorkingDir: process.cwd(), entryPoints: ["./src/index.jsx"], bundle: true, format: "esm", splitting: true, sourcemap: true, ignoreAnnotations: true, metafile: true, }).then((server) => { console.log("HTTP Server starts at port", server.port); }); } runBuild();
-
我们在浏览器访问 localhost:8000 可以看到 Esbuild 服务器返回的编译产物如下所示:

- 后续每次在浏览器请求都会触发 Esbuild 重新构建,而每次重新构建都是一个增量构建的过程,耗时也会比首次构建少很多(一般能减少 70% 左右)。
- Serve API 只适合在开发阶段使用,不适用于生产环境。
单文件转译——Transform API
-
除了项目的打包功能之后,Esbuild 还专门提供了单文件编译的能力,即 Transform API ,与 Build API 类似,它也包含了同步和异步的两个方法,分别是 transformSync 和 transform 。下面,我们具体使用下这些方法。
-
首先,在项目根目录新建 transform.js ,内容如下:
// transform.js const { transform, transformSync } = require("esbuild"); async function runTransform() { // 第一个参数是代码字符串,第二个参数为编译配置 const content = await transform( "const isNull = (str: string): boolean => str.length >