Skip to content

指南 · 专家

一、插件机制:onResolve / onLoad 与模块边界

esbuild 插件不暴露 AST,作用在「模块的解析与加载」两个边界上:

js
const envPlugin = {
  name: 'env',
  setup(build) {
    // ① 拦截路径解析:标记为虚拟模块(非 file namespace 不走文件系统)
    build.onResolve({ filter: /^env$/ }, () => ({
      path: 'env',
      namespace: 'env-ns',
    }))
    // ② 提供模块内容与解释方式
    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
      contents: JSON.stringify(process.env),
      loader: 'json',
    }))
  },
}

await esbuild.build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', plugins: [envPlugin] })
  • 插件 = { name, setup(build) };setup 每次 build 执行一次;
  • filter 是 Go 正则:在 Go 侧先行筛选、不必每个路径回调 JS——官方强调 filter 尽量收窄,这是插件性能的关键;
  • namespace:模块默认在 file namespace;自定义 namespace 表示「不在磁盘上」,虚拟模块内容里还有相对导入时要给 resolveDir
  • 生命周期钩子:onStart(每次构建开始)、onEnd(可读改 build result)、onDispose(清理);build.resolve() 可手动调用内置解析。

硬边界:插件只配 build API(transform 不行)、CLI 不能用插件没有 AST 操作 API——要做表达式级改写,在 onLoad 里自接 Babel/SWC 处理 contents。官方态度:「插件最适合做克制的、只定制构建一小方面的事;需求非常定制就该用别的工具。」

实战:onEnd 做构建通知 / 产物后处理

js
const notifyPlugin = {
  name: 'rebuild-notify',
  setup(build) {
    build.onStart(() => console.time('build'))
    build.onEnd((result) => {
      console.timeEnd('build')
      if (result.errors.length) console.error('失败:', result.errors)
      // 配 metafile: true 时这里能拿到 result.metafile 做体积守门
    })
  },
}

onEnd 与 context API 是绝配:ctx.watch() 下每次重建都会触发,等于免费获得「构建完成回调」——上层工具(dev server、测试 runner)大多靠它编排。

二、为什么快:FAQ 的四点拆解

  1. Go + 原生代码:JS 写的打包器每次启动都被 VM「第一次见到」,要边解释边收集优化提示;esbuild 是编译好的原生码,起跑线就不同;
  2. 极致并行:parse / link / codegen 三阶段都设计为吃满所有核;Go 线程共享内存,优于 JS worker 间序列化传数据;
  3. 一切自研、零三方依赖:自写 JS/TS 解析器,绕开官方 TS 编译器的性能陷阱(megamorphic 对象形状、不必要的动态属性访问);
  4. 内存效率:AST 全程只过约三遍(lex/parse/scope 一遍,bind/minify/transform 一遍,minify/codegen 一遍),数据尽量留在 CPU 缓存里。

推论:esbuild 的速度来自架构而非缓存——这也是它敢承诺「无缓存极速」的原因。同理,它拒绝暴露 AST 给插件:一旦节点反复进出 JS 层,性能模型就塌了(Babel 慢的根因之一)。

三、能力边界:官方「不做」清单

FAQ 明确有意不在核心实现(不是「还没做」):

不做替代方案
TypeScript 类型检查tsc --noEmit 并行跑
ES6+ → ES5 降级Babel / SWC 接在 esbuild 后
HMRVite / webpack(esbuild 只有整页 live reload)
模块联邦webpack / Rspack
其他语言(Vue/Svelte/Elm/Angular)社区插件或对应框架工具链
自定义 AST 操作 APIonLoad 里自接 Babel
.d.ts 生成tsc --emitDeclarationOnly(或 tsup 封装)
polyfill 注入core-js / preset-env

HTML 内容类型「在考虑中」(#31)。这种范围克制是设计哲学:esbuild 要做的是稳定的基础设施层,而不是 all-in-one 框架。

四、版本语义与维护状态(2026-06)

  • 0.28.x,仍是 0.x:官方约定 patch 向后兼容、minor 承载破坏性变更(0.27 → 0.28 要读 changelog)——所以安装要 --save-exact,CI 锁版本;
  • FAQ 自评「late-stage beta」:稳定且被大规模生产使用(Vite、Amazon CDK、Phoenix……),但拆分等能力仍「primitive」;
  • 维护态:作者 Evan Wallace(单一主维护者)原话「I'm not doing active feature development for esbuild at the moment」;收尾目标 = 拆分改进(#16)+ HTML(#31),之后「relatively complete」——esbuild 的终态是完成,不是无限演进。

五、生态位:被谁用、被谁追

  • Vite(7 及之前):dev 依赖预打包(optimizeDeps,CJS→ESM、合并细碎模块)+ TS/JSX 单文件 transform + 默认 JS/CSS minify 都是 esbuild;生产 bundle 用 Rollup——「双引擎」差异正是 Rolldown 要统一的问题;
  • tsup:基于 esbuild 的库打包事实标准(配 tsc 出 .d.ts);
  • Amazon CDK / Phoenix / Serverless 各框架:函数代码打包引擎;
  • Rust 追赶者:Rolldown(Rollup API 兼容、Vite 御用方向)、Rspack(webpack 兼容)、Oxc(解析/转换基建)——竞争逻辑都是「esbuild 证明了原生化的收益,我们再补上它刻意不做的部分」。

六、专家级易错点

  • minor 升级当 patch 升^0.27.0 这种 range 会把破坏性变更拉进来——锁精确版本;
  • 以为 esbuild 检查了类型:CI 里没跑 tsc --noEmit,类型错误带上生产;
  • --define:K=production 忘了 JSON 引号:被替换成标识符,运行时 ReferenceError
  • 在 cjs/iife 里用顶层 await:仅 esm 支持(require() 是同步的);
  • 指望 --target=es5:转不动直接报错,esbuild 不做 ES5 降级;
  • 拿 serve 当生产服务器:产物在内存、按请求构建,是纯开发工具;
  • 插件 filter 写 /.*/ 全量回调:Go→JS 往返吃掉性能红利,filter 必须收窄;
  • .babelrc 式直觉:esbuild 没有配置文件(也是有意的),一切显式传参——封装统一构建脚本即「配置」。