Agent Developer Roadmap
构建工具

esbuild:Go 原生转换器与打包器的能力边界

核验日期:2026-07-09。示例版本来自 examples/build-tools/package.jsonesbuild 0.28.1。esbuild 仍是 0.x 主线,本文按官方 API 和 FAQ 说明稳定行为,不把 0.x 简化为“不能生产使用”。

需要掌握的问题

主题读者必须回答的问题原理与边界高频错误代码或观察证据来源
定位与固定成本esbuild 在构建链路里负责哪几段?Go 原生实现把 parse、link、code generation、minify 和部分 bundling 集成在一个工具里;快来自并行和少做不必要工作,不等于功能覆盖所有上层工具把 esbuild 当 Vite/webpack 的完整替代,忽略 HTML、HMR、框架插件和类型检查共享示例的 Build API 配置和 benchmark 分组esbuild API
Transform API什么时候只需要把一段字符串转换成 JS?Transform API 接收内存字符串,隔离于文件系统,不解析依赖,不能 bundle,也不能用 plugins用 Transform API 期待解析 import、处理 CSS 或调用 onResolvebenchmark.mjsesbuild.transform(src/*.ts)Transform API
Build API什么时候需要 entry、依赖图、bundle 和产物文件?Build API 读取 entry points,按 loader/platform/format/target 解析、链接和输出,支持 bundle、splitting、metafile、plugins 与 context忘记 bundle: true 却期待依赖被内联;platform 写错导致条件导出错examples/build-tools/configs/esbuild.config.mjsBuild API
loader、platform、format、target这些选项分别改变什么?loader 决定输入类型,platform 决定解析条件和内置变量,format 决定输出模块形式,target 决定语法降级边界以为 target 自动补 polyfill;把 browser 平台构建成 node 条件.css loader、platform: 'browser'format: 'esm'target 示例Content Types
syntax transform 与类型检查TypeScript/JSX 处理到什么程度?esbuild 能删除 TS 类型并转换受支持语法;不会执行 TypeScript 类型检查,也不会按 tsc 完整语义报类型错误构建成功后上线运行时错误;把 tsconfig 当类型检查入口catalog.ts 类型被删除,artifact test 只验证输出形态TypeScript content type
Tree Shaking 与 code splitting未使用导出和动态导入如何进入产物?esbuild 在 bundling 模式下基于 ESM 做 Tree Shaking;splitting: true 目前要求 format: 'esm' 并输出到 outdirCommonJS 依赖无法理想 tree shake;HTML 直接加载 async chunkmetafile.json 证明 detail.tsdynamic-importTree shaking
metafile 与分析怎样证明产物结构,而不是只看退出码?metafile: true 输出 inputs/outputs/imports/bytes,适合做产物断言和大小分析手写 hash 文件名、只看 dist 文件列表、忽略 dynamic import 边artifact test 读取 dist/esbuild/metafile.jsonMetafile
plugins:onResolve/onLoadesbuild 插件能改哪里,不能改哪里?插件通过 setup 注册 onResolveonLoadonStartonEnd 等回调;filter/namespace 决定命中范围filter 写太宽拖慢构建;插件里做类型检查或复杂全局优化最小虚拟模块插件和边界表Plugins
context/rebuild/watch/serve增量、监听和本地服务有什么区别?context() 保存构建选项和内部状态;rebuild 手动重建,watch 自动重建,serve 提供最近构建结果把 live reload 当 HMR;忘记 dispose();serve 当生产服务器context 示例和故障表Rebuild, watch, serve
性能与重大变化官方性能数字怎样引用才不误导?官方 benchmark 有固定输入、机器和版本;本仓库 benchmark 只说明测量方法,不排名所有项目用一次 wall time 宣称“最快”;混测 cold/hot/cachebenchmark.mjs 的 JSON 输出和官方 FAQ 条件esbuild FAQ

内容边界说明:

分类本文落点
编程语言与类型系统解释 JS/TS/JSX 语法解析、类型删除、target 降级和类型检查边界;不教学 TypeScript 类型系统本身
Browser APIs 与持久化能力覆盖浏览器 ESM、dynamic import、CSS、Source Map、live reload 与静态资源路径;不讨论 IndexedDB、Storage 或权限 API
UI 框架与元框架只说明 JSX/loader/插件入口和上层框架工具链边界;组件渲染、水合、状态管理不在本文重复
构建工具与运行时覆盖输入、parse、link、code generation、bundle、plugins、context、metafile、产物和故障定位
后端、数据与 Agent 技术只覆盖插件和构建脚本以开发者权限执行、define/env 泄露和 CI 资源边界;请求生命周期、数据库一致性和 Agent 评测不适用

esbuild 的主线:少层封装,直接完成常见转换和打包

esbuild 的懒办法是:如果你只需要“把 TS/JSX 转成 JS、快速打一个浏览器 bundle、拿到机器可读产物元数据”,直接用它;如果你需要复杂 HTML 模板、框架 HMR、Module Federation 或上百个生态插件,别把这些能力硬塞给 esbuild。

官方 FAQ 对“为什么快”的解释有两个关键点:esbuild 用 Go 写成并编译为原生代码,命令行启动时不需要像 JS bundler 一样先让 Node 解析 bundler 自己的大量 JavaScript;同时 Go 的共享内存和 goroutine 让它更容易把编译工作铺满 CPU 核心。esbuild 的内部流程可以粗略拆成 parse、link、code generation 三段:parse 和 code generation 占大头且高度可并行,link 主要负责符号绑定和图连接,天然更偏串行。JS API/CLI 调用时,Node 侧只是把 build/transform 请求送进 esbuild 的原生服务进程,真正的解析、链接和代码生成在 Go service 内完成。

和相邻工具的边界:

工具更适合不适合
esbuild Transform API单文件 TS/JSX 转换、快速 minify、内存字符串处理依赖解析、CSS 图、插件、bundle
esbuild Build API小到中型浏览器 bundle、CLI/脚本化构建、metafile 分析框架开发服务器、复杂生态插件兼容
SWC编译转换、宿主工具内核、Rust/Wasm 插件应用级 bundling、HTML/CSS 产物
Vite/Rspack/webpack开发服务器、HMR、框架插件、复杂应用构建单纯 transform 场景

并行模型的边界:

阶段并行性说明
parse多个输入文件可并行词法分析、解析、建立局部符号
link低到中需要跨模块绑定符号、连接 dependency graph,很多决策依赖全局图
code generation多个 chunk/文件可并行打印代码和 Source Map
JS API 调用层Node 只发请求和接收结果,不应把重工作放在插件回调里

严重问题完整流程:

症状根因观察命令修正
import 仍留在 Transform 输出里Transform API 不 bundle,也不读文件图用最小 transform("import x from './x'") 复现需要依赖图时改 Build API
老浏览器缺 PromiseMap 或 DOM APItarget 只处理语法,不自动补运行时 API搜索产物是否有 polyfill 模块应用入口或上层工具显式管理 polyfill
页面点击后 async chunk 404HTML 或部署路径没跟 esbuild 输出结构对齐构建后静态预览并点击按钮metafile 生成准确 HTML,验证 dynamic import

Transform API:隔离字符串转换,不看文件系统

官方把 Transform API 定位为 Build API 的受限特例。它接收一段代码字符串,返回转换后的代码和 map。优点是简单、快、可在浏览器 Wasm 版本中使用;代价是它和文件系统断开,不会读取附近 package.json、不会解析 import、不能 bundle、不能用 plugins。

import { transform } from 'esbuild';

const result = await transform('let x: number = 1', {
  loader: 'ts',
  format: 'esm',
  sourcemap: true,
  target: 'es2022',
});

console.log(result.code); // let x = 1;

本仓库 benchmark 用 Transform API 处理所有 .ts 文件:

const result = await transform(source, {
  loader: 'ts',
  format: 'esm',
  sourcemap: true,
  target: 'es2022',
});

边界表:

能力Transform APIBuild API
输入内存字符串entry points、stdin、文件系统
依赖解析不解析解析并可 bundle
plugins不支持支持 async API plugins
CSS bundle不支持支持 loader 与 CSS 输出
metafile不支持支持
典型用途编译片段、在线编辑器、minify 单文件应用/库构建、产物分析

错误完整流程:

症状根因观察命令修正
onResolve 从不触发Transform API 不能用 plugins检查调用的是 transform() 还是 build()切换到 Build API
输出仍有 import './style.css'Transform 不知道 CSS 文件打印 result.code用 Build API 的 loader: { ".css": "css" }
tsconfig paths 未按预期解析Transform 与文件系统隔离最小 transform fixture用 Build API 或上层工具解析路径

Build API:从 entry 建图、链接、输出

共享示例使用 Build API,因为它需要浏览器 bundle、CSS、Source Map、metafile 和动态导入。

import { build } from 'esbuild';

const result = await build({
  entryPoints: ['src/main.ts'],
  outdir: 'dist/esbuild/assets',
  bundle: true,
  splitting: true,
  format: 'esm',
  platform: 'browser',
  sourcemap: true,
  minify: true,
  metafile: true,
  loader: { '.css': 'css' },
});

这些选项分别解决不同问题:

选项作用错配后表现
entryPoints图入口没有主 JS 或入口不含业务代码
bundle: true递归内联/链接依赖import 留给浏览器,CSS/dynamic 边界不完整
splitting: true允许 dynamic import 输出 async chunkdetail.ts 被合并或无法形成异步边界
format: 'esm'输出 ESM,支持 splitting非 ESM format 下 code splitting 不成立
platform: 'browser'使用浏览器条件和内置行为打进 Node-only 入口或 process 泄露
loader: { '.css': 'css' }把 CSS 作为构建输入CSS 没生成 asset
metafile: true产出机器可读图无法自动验证 dynamic import 和大小

示例脚本用最少代码生成 HTML,不引入 HTML 插件:

const main = Object.entries(result.metafile.outputs)
  .find(([, output]) => output.entryPoint === 'src/main.ts')?.[0];
const css = result.metafile.outputs[main].cssBundle;

await writeFile('dist/esbuild/index.html', `<!doctype html>
<script type="module" src="./${toOutputPath(main)}"></script>
<link rel="stylesheet" href="./${toOutputPath(css)}">
`);

这里故意不写通用 HTML 生成器。示例只需要证明 esbuild 产物能被浏览器加载,metafile 已经给了主 JS 和 CSS 路径。

loader、platform、format、target:四个选项别互相替代

这四个选项最容易被混成“兼容配置”:

选项输入/输出层解决的问题不解决
loader输入解析.ts.tsx.css.json 等如何被读取目标浏览器支持范围
platform模块解析和内置语义browser/node/neutral 条件、默认 format、主字段polyfill 注入
format输出模块形式esmcjsiife是否解析依赖图
target语法转换降级部分 JS/CSS 语法运行时 API、类型检查

严重问题完整流程:

症状根因观察命令修正
浏览器报 process is not definedplatform 或条件导出选到 Node 入口搜索产物 process.、检查 package exports设置 platform: 'browser',必要时 alias 到 browser 入口
低版本浏览器仍报缺 APItarget 不注入 polyfill搜索产物是否有 core-js 或 polyfill上层应用统一引入 polyfill
动态分块未出现splittingformatoutdir 条件不满足检查 metafile.outputs使用 format: 'esm'splitting: trueoutdir

syntax transform、polyfill 和类型检查是三条线

esbuild 可以把 TypeScript 语法擦除成 JavaScript:

type Tool = { name: string };

export function label(tool: Tool): string {
  return tool.name.toUpperCase();
}

预期输出要点:

export function label(tool) {
  return tool.name.toUpperCase();
}

它不会证明调用方传入的对象有 name,也不会给 PromiseMapArray.prototype.at 注入 polyfill。

需求esbuild 能做仍需要
删除 TS 类型是,loader 为 ts/tsxtsc --noEmit 做类型检查
JSX transform是,配置 JSX 相关选项框架运行时和插件语义
语法降级是,按 target运行时 API polyfill
类型错误阻塞 CITypeScript checker
decorator/emit metadata 等 TS 特性有边界,不等价完整 tsc按项目需求保留 tsc 或 Babel/SWC 插件

严重问题完整流程:

症状根因观察命令修正
const n: number = "x" 构建通过esbuild 转译 TS,不做类型检查单独运行 tsc --noEmitCI 保留 TypeScript 类型检查
老浏览器缺 Array.prototype.attarget 只改语法,不补 API浏览器报错或搜索 polyfill入口引入 polyfill 或调整兼容策略
decorator 行为和 tsc 不同依赖 TS 编译器专有语义最小 decorator fixture 对比保留 tsc/Babel/SWC 对应链路

Tree Shaking、code splitting 和 metafile

共享入口里有一个未使用导出和一个必须保留的副作用:

export const removableMarker = 'SHOULD_NOT_REACH_BUNDLE';
globalThis.__BUILD_TOOLS_SIDE_EFFECT__ = 'preserved';

Build API 的生产 bundle 应删除前者、保留后者。artifact test 读取所有 JS 验证:

观察点预期结果
SHOULD_NOT_REACH_BUNDLE不出现在 JS 中
preserved出现在 JS 中
detail.ts作为 dynamic-import 对应的 async output
HTML只加载 initial JS/CSS,不直接加载 async chunk

metafile 是最省事的机器证据。它记录 inputs、outputs、imports、entryPoint、bytes 等信息。共享测试用它判断主产物是否以 dynamic-import 指向 detail.ts 的输出:

const outputs = result.metafile.outputs;

for (const [owner, output] of Object.entries(outputs)) {
  if (output.entryPoint !== 'src/main.ts') continue;
  console.log(output.imports.filter((item) => item.kind === 'dynamic-import'));
}

严重问题完整流程:

症状根因观察命令修正
未用导出仍在 bundle输入不是 ESM、动态访问导出、禁用 minify/tree shakingrg "SHOULD_NOT_REACH_BUNDLE" dist/esbuild保留 ESM,减少动态导出访问
副作用被删错误标注 sideEffects: false 或 pure 注释过激搜索 preserved精确声明副作用文件
async chunk 被 HTML 直接加载HTML 生成逻辑误读 metafile outputsrg "detail" dist/esbuild/index.htmlHTML 只引用主 JS 和 CSS,dynamic 由运行时加载

plugins:onResolve/onLoad 是边界,不是万能中间层

esbuild 插件通过 setup(build) 注册回调。最常用的是 onResolveonLoad

const virtualPlugin = {
  name: 'virtual-demo',
  setup(build) {
    build.onResolve({ filter: /^virtual:info$/ }, (args) => ({
      path: args.path,
      namespace: 'virtual',
    }));

    build.onLoad({ filter: /.*/, namespace: 'virtual' }, () => ({
      contents: 'export const name = "esbuild";',
      loader: 'js',
    }));
  },
};

插件设计要点:

机制作用常见错误
filter用正则快速筛选请求写成 /.*/ 导致每个路径都进 JS 回调
namespace把虚拟模块和真实文件隔离所有模块混在 file namespace
onResolve决定路径、external、namespace在这里读完整源码
onLoad返回 contents/loader/watchFiles忘记登记外部依赖导致 watch 不更新
onEnd查看 errors/warnings/metafile修改已写产物而不更新 map/hash

插件边界:

能力支持情况
Build API async plugins支持
Transform API plugins不支持
Build sync API plugins不支持
跨模块高级优化能做一部分,但不如 Rollup/webpack 插件生命周期完整
安全隔离不提供;插件是构建时代码,能读文件和环境变量

严重问题完整流程:

症状根因观察命令修正
构建突然很慢插件 filter 过宽,每个路径都进 JS 回调打印命中次数收窄 filter/namespace
watch 不响应虚拟模块源文件onLoad 读取外部文件但没返回 watchFiles修改外部文件看是否 rebuild返回 watchFiles/watchDirs
CI secret 出现在 bundle插件读取 process.env 写入 contents`rg "sk_password

context/rebuild/watch/serve:live reload 不是 HMR

Build API 适合一次性构建。开发集成通常用 context()

const ctx = await esbuild.context({
  entryPoints: ['src/main.ts'],
  outdir: 'dist/esbuild/assets',
  bundle: true,
});

await ctx.watch();
const result = await ctx.rebuild();
const server = await ctx.serve({ servedir: 'dist/esbuild' });
await ctx.dispose();

官方文档把增量相关能力分成三类:watch 自动重建、serve 提供最近构建结果、rebuild 手动重建。watch + serve 可以实现 live reload,但这不是 Vite/webpack 意义上的 HMR。

能力esbuildVite/webpack HMR
文件变化后重建支持 watch/rebuild支持
浏览器自动刷新需要 live reload 集成内置或插件支持
模块级状态保留不提供完整 HMR 协议通过 import.meta.hot 或 runtime
框架边界更新不负责框架插件负责

错误完整流程:

症状根因观察命令修正
修改代码后页面不更新只调用 build,没有 context/watch看脚本是否使用 context()使用 watch 或上层 dev server
进程退出后端口还占用忘记 ctx.dispose()lsof -i :<port>在 shutdown 钩子里 dispose
团队以为 live reload 会保留状态live reload 是整页刷新浏览器状态丢失需要状态保留就用 Vite/webpack HMR

共享示例:esbuild Build API 构建浏览器入口

运行命令:

pnpm --filter @roadmap/build-tools-slice build:esbuild

输入:

examples/build-tools/src/main.ts
examples/build-tools/src/catalog.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/side-effect.ts
examples/build-tools/src/styles.css

预期结果:

examples/build-tools/dist/esbuild/index.html
examples/build-tools/dist/esbuild/metafile.json
examples/build-tools/dist/esbuild/assets/*.js
examples/build-tools/dist/esbuild/assets/*.css
examples/build-tools/dist/esbuild/assets/*.js.map
examples/build-tools/dist/esbuild/assets/*.css.map

验证观察:

find examples/build-tools/dist/esbuild -type f | sort
node --test --test-name-pattern esbuild examples/build-tools/test/artifacts.test.mjs

预期输出要点:

观察点预期结果
index.html通过相对 script type="module" 加载主 JS,通过 link rel="stylesheet" 加载 CSS
metafile.json主 JS 的 imports 以 dynamic-import 指向 detail.ts 产物
JS source map主 chunk 指回 main.ts,async chunk 指回 detail.ts
CSS source map指回 styles.css
bundled JS包含 Babelpreserved,不包含 SHOULD_NOT_REACH_BUNDLE

性能、迁移和重大变化:只引用有条件的数字

esbuild 的官方 FAQ 解释了它为什么快,也给了 benchmark details。引用这类数字时必须保留条件:输入规模、机器、版本、是否 minify、是否 bundle、是否 warm cache。本文不从本仓库小样例推出普遍排名。

官方 benchmark 条件要一起引用,否则数字没有意义:

官方 benchmark输入esbuild 选项对照工具条件机器与统计方式
JavaScript benchmarkthree.js 复制 10 次,约 547,441 行,冷构建单 bundle--bundle --minify --sourcemapRollup 4 加 terser;webpack 5 用 production + sourcemap;Parcel 2 默认6-core 2019 MacBook Pro、16GB RAM、关闭 Spotlight;取 3 次中的最短耗时
TypeScript benchmark旧 Rome TypeScript 代码库,约 131,836 行,输出单个可运行 minified bundle + Source Map--bundle --minify --sourcemap --platform=nodewebpack 5 用 ts-loadertranspileOnly: true、production + sourcemap;Parcel 2 用 package engines.node同一台 6-core 2019 MacBook Pro、16GB RAM、关闭 Spotlight;取 3 次中的最短耗时

这些条件说明两件事:第一,官方数字测的是冷构建大输入,不是小项目热更新;第二,TypeScript 对照明确关闭类型检查,所以不能把结果解释成“esbuild 比带类型检查的完整 TypeScript 构建快多少”。

本仓库的测量命令:

pnpm --silent --filter @roadmap/build-tools-slice benchmark -- --iterations=1 --warmup=0

它把 esbuild 分成两项:

分组命令解释
转换器esbuild.transform(src/*.ts)只测 Transform API 对 TS 文件的内存转换
打包器pnpm run build:esbuild测 Build API 从入口到 browser bundle

0.x 迁移的实际策略:

变化类型风险最小迁移动作
默认 target 或语法支持调整产物语法变化显式写 target,跑旧浏览器 fixture
插件 API 行为变化自研插件输出变化给插件保留最小 fixture,检查 onResolve/onLoad
tree shaking/minify 变化副作用被误删或体积变化搜索可观察副作用和 dead marker
metafile schema 变化自动分析脚本误判metafile.outputs 加结构断言

严重问题总表:

严重问题表现根因最小观察修正
Transform 当 Build 用依赖、CSS、插件都不生效API 选错看调用是 transform 还是 build改 Build API
target 当 polyfill老环境缺 API语法转换和运行时 API 混淆浏览器报错或搜索 polyfill单独管理 polyfill
live reload 当 HMR页面状态丢失esbuild 没提供框架 HMR 边界修改模块看状态是否保留用 Vite/webpack HMR
metafile 不验收async chunk 或 CSS 路径错只看命令退出 0artifact test 读 metafile.json用 metadata 驱动 HTML/测试
插件泄露 secret插件读取 env 写入 bundle构建代码信任边界失控`rg "secretpassword

资料入口

在 GitHub 上编辑此页

本页目录