构建工具决策表:先固定问题,再比较实现
核验日期:2026-07-10。各工具使用
examples/build-tools中的同一份输入;版本以示例package.json为准。任何性能结论都必须重新记录机器、版本、缓存状态、样本和产物契约。
需要掌握的问题
| 主题 | 必须回答的问题 | 原理与边界 | 高频错误 | 证据与来源 |
|---|---|---|---|---|
| 三类问题 | 当前要解决语法转换、应用打包还是库发布? | 转换器处理单模块;应用工具还要管理开发服务器、模块图和浏览器入口;库工具优先保证 exports、external 与多格式消费 | 把八个工具放进一张“速度榜” | 共享示例把 transformer 与 bundler 分组;esbuild Transform API |
| 职责与宿主 | 谁拥有配置、resolve、transform、chunk、HTML 和运行时? | Babel/SWC 常嵌入宿主;Vite 是工作流;webpack/Rspack、Rollup/Rolldown、esbuild Build 是打包底座 | 为单段性能替换整条工具链 | 八篇文章与本页职责对照 |
| 性能条件 | 冷构建、热更新、增量构建还是 CI 吞吐超标? | 不同阶段复用的状态不同;原生实现、JS 插件跨边界、I/O、Source Map 和 minify 都会改变结果 | 用四个模块的小样例宣布普遍排名 | 示例 benchmark 输出机器、版本、samples 和 median;esbuild FAQ |
| 可靠性 | 如何证明解析、图、分块、Source Map 和浏览器加载都正确? | 命令退出 0 只证明流程未抛错;结构化 metadata 与浏览器验收才证明产物契约 | 只比耗时或文件字节数 | artifacts.test.mjs 验证 HTML、async chunk、metadata、map 和路径 |
| 缓存与 HMR | 哪一层缓存什么,失效由谁负责? | 转换缓存、模块图缓存、持久化缓存与 HMR runtime 是不同状态机 | 把 watch、live reload、HMR 混为一谈 | webpack cache、Vite HMR API |
| 插件生态 | 插件 API 兼容是否等于行为完全一致? | hook 名称可以兼容,调用时机、上下文、内建模块与输出语义仍可能不同 | 不跑真实插件 fixture 就迁移引擎 | Rspack plugin compatibility、Rolldown Plugin API |
| 产物与发布 | 应用和库分别要交付什么? | 应用交付 HTML、hash assets、runtime/manifest;库交付 exports、类型、external 策略和消费端兼容 | 用“能生成一个 JS”替代发布验收 | Rollup configuration、共享 artifact tests |
| 迁移成本 | 能否按阶段替换并回滚? | 配置相似只是表面;自定义 Loader/Plugin、HMR、SSR、Module Federation 和发布脚本决定成本 | 一次性替换开发、构建、测试与部署 | webpack→Rspack、Rollup→Rolldown 分层迁移表 |
| 安全边界 | 构建插件和 Source Map 接触哪些秘密? | 配置与插件以 Node 权限执行;公开 map、define/env 和远程插件可能泄露源码或凭据 | 给不可信 PR 构建注入生产 secret | Vite env boundary、各文章安全章节 |
这是一篇工具选型文章,编程语言的数据结构、浏览器持久化、UI 可访问性不属于它的直接职责;但浏览器加载、运行时 chunk、Source Map 暴露和插件代码执行会影响构建结果,因此必须纳入验收与信任边界。
先固定语法转换、应用打包、库发布三类问题
“构建工具”至少包含三种不同采购问题。先选问题,再选候选;否则会把 Babel 的 AST 插件能力、Vite 的开发体验和 Rollup 的库发布能力当成同一维指标。
| 问题 | 输入 | 必要输出 | 不属于最低职责 | 首选候选集 |
|---|---|---|---|---|
| 语法转换 | 单个 JS/TS/JSX 字符串或文件 | 目标 JS、诊断、Source Map | 完整依赖图、CSS、HTML、类型证明 | Babel、SWC、esbuild Transform |
| 应用打包 | HTML/JS entry、资源、依赖与环境配置 | initial/async chunks、CSS/assets、HTML 或 manifest、加载 runtime | 业务运行时正确性、后端授权 | Vite、webpack、Rspack、esbuild Build;底层 Rolldown |
| 库发布 | 一个或多个公共入口、peer 依赖、类型声明 | ESM/CJS 等约定格式、exports 对齐、map、external 保留 | 完整框架 dev server、应用 HTML | Rollup、Rolldown;受限场景用 esbuild/Vite library mode |
一家公司可以同时使用三类工具:例如 Babel 做遗留语义插件,Vite 管应用开发与生产构建,Rollup 管独立库发布。这不是重复,而是职责组合;真正的重复是多个阶段都在无意中转同一语法、重复压缩或各自解释 targets。
八个工具的职责地图
| 工具 | 核心职责 | 配置与插件入口 | 缓存/HMR | 典型产物 | 主要固定成本 |
|---|---|---|---|---|---|
| Babel | parser → AST traverse/transform → generator;按 targets 降级语法 | preset、plugin、visitor、babel.config.* | 自身常由宿主缓存;无应用 HMR runtime | 逐文件 JS 与 Source Map | 插件顺序、配置发现、polyfill/类型边界 |
| SWC | Rust parser/transform/codegen;TS/JSX 与兼容转换 | .swcrc、JS API、Wasm plugin、宿主 Loader | 宿主决定增量与 HMR | 逐文件 JS 与 Source Map | native binary、Wasm ABI、宿主配置映射 |
| esbuild | Transform API 做隔离转换;Build API resolve、link、打包 | JS/Go API、CLI、onResolve/onLoad | context 支持 rebuild/watch/serve;live reload 不等于框架 HMR | JS/CSS chunks、map、metafile;HTML 需自建/插件 | 插件阶段较少,生态与高级应用策略需补齐 |
| Vite | HTML-first 开发服务器、按请求转换、HMR、生产构建工作流 | Vite/Rollup-compatible plugin、Environment API | dev module graph、依赖预构建、HMR runtime | HTML、hash assets、.vite/manifest.json | dev/prod 双路径、框架插件与部署 base |
| webpack | 通用应用模块图、Loader/Plugin 生命周期、chunk/runtime | Loader、Tapable Plugin、Compiler/Compilation | memory/filesystem cache、watch、成熟 HMR | HTML、JS/CSS assets、runtime、stats | 配置面广、历史兼容、自定义扩展维护 |
| Rspack | webpack 兼容的 Rust 应用 bundler | webpack-like config、Loader、兼容 Plugin、built-ins | incremental、persistent cache、HMR、lazy compilation | 与 webpack 类似的 assets/runtime/stats | 兼容组合、native 包、插件跨 JS/Rust 边界 |
| Rollup | 静态 ESM 图、Tree Shaking、库格式与输出钩子 | resolveId/load/transform、输出 hooks | watch 复用 bundle cache;不提供完整框架 HMR | ESM/CJS/UMD chunks、assets;HTML/manifest 由插件生成 | external、exports、side effects、多输出设计 |
| Rolldown | Rust + Oxc 的 Rollup-compatible bundler,内建更多转换/分析 | Rollup-compatible hooks、hook filters、built-ins | watch API;独立使用不等于 Vite HMR | chunks/assets;HTML/manifest 要显式配置 | 兼容差异、native/Wasm、快速演进期回归 |
“插件生态大”也不能单独决策。要区分可安装数量、目标插件是否兼容、维护活跃度、hook 语义、插件是否进入热路径,以及团队是否拥有插件源码。一个只依赖官方框架插件的新应用,与拥有二十个自定义 webpack Plugin 的存量平台,迁移成本完全不同。
语法转换:Babel、SWC 与 esbuild Transform
| 维度 | Babel | SWC | esbuild Transform |
|---|---|---|---|
| 职责深度 | 支持深度 AST visitor/preset 定制,适合 codemod、提案与遗留语义插件 | 常见 TS/JSX/React 与 targets 转换,适合作为宿主的高吞吐编译段 | 内存字符串的快速常见转换、minify 与 loader |
| 性能条件 | JS 插件数、AST 遍历次数、helper/map 会影响耗时 | native 调用、文件规模、Wasm 插件跨边界影响耗时 | 单次进程启动与批量 API 的差异很大 |
| 可靠性 | 生态成熟、可观测配置强;插件顺序可能改变语义 | 内建路径减少 JS 插件差异;native/Wasm 版本要锁定 | API 面小、默认统一;复杂插件需求可能被迫在外层补胶水 |
| 使用难度 | 低门槛 preset,高阶 visitor/配置发现难 | 常规配置短,宿主与 Babel 选项映射需验证 | Transform API 最小;超出其职责后要切 Build 或宿主 |
| 维护成本 | 多插件升级与 Babel major 迁移成本 | 核心配置少,但 binary/ABI 与平台组合有成本 | 配置少;自研缺失 hook 会把成本转移到外围脚本 |
| 适用规模 | 规模不是关键;语义定制复杂度才是关键 | 大量模块的通用转换段 | 编辑器、测试器、文档工具中的隔离转换或轻量宿主 |
| 迁移成本 | 自定义 Babel plugin 越多越高 | 从 Babel 迁移要逐插件确认语义,不只对比语法能否解析 | 只覆盖 Transform 的项目低;依赖 Babel visitor 的项目高 |
选择顺序:先列出必须保留的语义插件、decorator/class fields 假设、targets、module format、JSX runtime、Source Map 与类型检查;再用真实源码 fixture 比较输出。若只需删除 TypeScript 类型并保留 ESM,三者都可能完成;若要自定义 AST 分析或尚无等价实现的 Babel plugin,转换耗时不是唯一约束。
共享示例故意证明边界,而不是证明谁更强:Babel 8.0.1 和 SWC 1.15.43 都把四个 .ts 文件转成四个 .js 与 map,保留 ESM,不生成 CSS、HTML 或 bundle。benchmark 中的 esbuild Transform 也只转换内存中的 TS;它不能与六个完整 bundler 的总耗时直接排名。
应用打包:Vite、新旧 webpack/Rspack 与嵌入式 esbuild
新应用先比较工作流,不只比较 bundler
| 约束 | Vite | webpack 5 | Rspack 2 | esbuild Build | 独立 Rolldown |
|---|---|---|---|---|---|
| 开箱开发服务器/HMR | 强,HTML-first、框架插件成熟 | 强,dev-server 与 HMR 成熟 | 强,面向 webpack 工作流兼容 | serve/watch 可用;框架 HMR 需上层 | watch 可用;通常由 Vite 提供应用工作流 |
| 复杂资源与历史插件 | 依赖 Vite/Rollup 插件与框架集成 | 在这组候选中 Loader/Plugin 的历史覆盖较广 | 常见 webpack 面兼容,长尾要查兼容说明 | 核心 loader 强,复杂插件点较少 | Rollup 插件兼容度高,仍需检查差异 |
| 持久化/增量 | dev module graph 与依赖预构建;由框架/插件共同影响 | filesystem cache、snapshot、watch | incremental、persistent cache、lazy compilation | context/rebuild/watch | watch cache;完整 dev 缓存策略由宿主补齐 |
| 大型存量迁移 | 要重画配置、HMR 和插件边界 | 保持现状风险最低但性能改造需证据 | 配置相似,适合分层验证迁移 | 通常需要重写插件与 HTML/runtime 工作流 | 若原系统是 Rollup/Vite 更自然 |
| 应用产物 | HTML、assets、manifest | HTML、assets、runtime、stats | HTML、assets、runtime、stats | chunks/assets、metafile;示例脚本生成 HTML | chunks/assets;示例插件生成 HTML/manifest |
Vite 是上层开发与构建产品,Rolldown 是 Vite 8 当前的生产打包底座;两者不能用“同一个 Rust 引擎”画等号。Vite 还拥有 HTML 入口、dev module graph、依赖优化、HMR、环境与框架插件。独立 Rolldown 适合需要底层控制或构建工具作者,不是自动替代 Vite 配置。
webpack 还是 Rspack:先做兼容资产清点
webpack 5 与 Rspack 2 的选择通常发生在存量应用。先导出下列清单:
module.rules中每个 Loader,是否使用 pitch、异步 callback、额外依赖或自定义 metadata。- 每个 Plugin 使用哪些 Compiler/Compilation/Tapable hooks,是否操作 webpack 内部对象。
- dev-server、HMR accept/dispose、Module Federation 与 SSR 的运行时契约。
- filesystem cache 的 build dependencies、snapshot、跨分支复用策略。
- stats、bundle analyzer、部署 manifest、publicPath 和 CDN 发布脚本。
Rspack 官方 webpack 迁移指南明确要求 filesystem cache 从 webpack 的 type: 'filesystem' 映射为 type: 'persistent',字段结构也不同;配置相似不代表可复制缓存对象。迁移应让 webpack 与 Rspack 在同一 commit 双构建,比较 artifact、stats、浏览器测试和性能样本,再逐包切 CI 与本地开发。若关键插件处于部分兼容或不兼容状态,应对照官方 Plugin compatibility 表选择替代插件或保留 webpack,而不是绕过内部 API。
esbuild 最适合被嵌入时
esbuild Build API 在共享示例中能生成 JS/CSS、动态 chunk、Source Map 与 metafile.json,外围配置再据 metadata 写 index.html。这说明它适合作为平台、CLI、测试器或自研框架中的嵌入式引擎:宿主明确拥有 HTML、HMR、缓存、部署和框架语义。
如果团队只想“少写配置”却没有意愿维护宿主层,直接采用 Vite 或框架官方构建方案通常维护成本更低。esbuild 的 API 简洁不会自动消除 HTML 注入、CSS 顺序、legacy browser、错误 overlay、HMR 边界和部署 manifest 等应用职责。
库发布:Rollup、Rolldown 与替代路径
| 维度 | Rollup 4 | Rolldown 1 | esbuild | Vite library mode |
|---|---|---|---|---|
| 主要优势 | 成熟 ESM 图、输出格式、Tree Shaking 与插件钩子 | Rollup-compatible API、Rust/Oxc 内建能力、面向 Vite 演进 | API 简洁、常见格式与转换吞吐 | 已在 Vite 项目中,复用 alias/框架插件与配置 |
| 可靠性条件 | 正确 external、moduleSideEffects、map 和消费 fixture | 除同项外,还要验证 hook 调用差异和兼容表 | 检查 CJS/ESM interop、插件缺口与产物约定 | 检查库模式默认值、CSS 与框架插件行为 |
| 插件生态 | Rollup 官方/社区生态成熟 | 可复用多数 Rollup 插件,但不是逐字节等价 | onResolve/onLoad 模型不同 | Vite/Rollup-compatible 插件,dev hook 与 build hook 有差异 |
| 多输出与 exports | 强,适合显式设计多入口、多格式 | API 相近;多 output 与 hook 状态要回归 | 可做,但复杂发布常需脚本组合 | 可做,适合应用仓中的中小库 |
| 迁移成本 | 现有 Rollup 项目最低 | 先兼容扫描、双构建、消费测试 | 从 Rollup 插件体系迁移较高 | 从 Vite 应用抽库较低,独立发布治理仍需补齐 |
库构建的完成条件不是 dist/index.js 存在。至少要检查:
package.json#exports的每个 public subpath 都有文件,未声明的内部文件不能被误依赖。peerDependencies与external一致,React/Vue 等单例运行时不会被重复打包。- ESM/CJS 消费 fixture、Node 与目标 bundler 都能加载对应条件。
.d.ts与 JS 入口、Source Map、CSS/assets 的发布清单一致。- dynamic import、top-level await、side effects 与 tree shaking 在消费者中保持语义。
Rollup→Rolldown 迁移的低风险边界是保留 input/output 约定,先逐个插件验证 resolveId/load/transform/generateBundle,再比较产物和消费者;Rolldown Plugin API 的差异说明列出了多 output、watch 与 hook 顺序的行为差异。不要因为 API compatible 就跳过 fixture。
性能比较必须写清条件
性能不是一个数。至少拆成以下指标:
| 指标 | 固定条件 | 反映什么 | 不能推出什么 |
|---|---|---|---|
| 冷启动/冷构建 | 清理工具缓存与 OS 缓存策略、固定依赖安装 | CI 首次构建和新开发机体验 | 热更新速度、长期产物质量 |
| 热构建/增量 | 先完成一次构建,固定修改同一模块 | 图复用、缓存失效与局部重算 | 全量构建吞吐 |
| HMR 延迟 | 固定浏览器、页面路径、更新模块和 accept 边界 | 从保存到浏览器应用更新的链路 | 生产构建速度 |
| 峰值内存/CPU | 固定并发、Source Map、minify 和 worker | CI 容量与开发机争用 | 用户运行时性能 |
| 产物字节/请求 | 相同 target、minifier、split 策略、压缩算法 | 网络与缓存候选成本 | 浏览器解析/执行一定更快 |
| 正确性通过率 | 相同 artifact 与浏览器测试 | 优化是否保持语义 | 所有业务路径都正确 |
共享命令只提供测量方法:
pnpm --silent --filter @roadmap/build-tools-slice benchmark -- --warmup=1 --iterations=5预期输出是一份 JSON:包含 Node、pnpm、工具版本、CPU、内存、每次 samplesMs、median/min/max 和 outputBytes;transformers 与 bundlers 分组。当前 benchmark 的 target、minify、Source Map 与产物职责并未完全统一,因此 outputBytes 不能跨工具横向比较,也不能据此给工具排产物体积名次。它只有四个源码模块,无法代表十万模块 monorepo、复杂框架 HMR 或 CI 远程缓存。真实横评应先统一 target、minify、Source Map、entry、split 和 side-effect 契约,再使用代表性入口重复至少三轮并保存原始 samples,而不是只抄 median。
可靠性、学习难度与维护成本要一起算
| 成本来源 | 早期看起来 | 长期故障 | 可审查证据 |
|---|---|---|---|
| 内建能力 | 配置更短 | 默认升级可能改变语义 | 锁版本、升级 diff、产物 fixture |
| 社区插件 | 很快补齐功能 | 维护停滞、供应链、hook 不兼容 | owner/Release、最小插件测试、依赖审计 |
| 自研插件 | 完全贴合业务 | 只有少数人懂内部生命周期 | 设计说明、单元/集成 fixture、退出方案 |
| 多段编译 | 每段都能解决局部问题 | targets、module、map、minify 重复或冲突 | 打印最终 config、记录每阶段输入输出 |
| 缓存 | 第二次构建很快 | 错误命中、跨平台/分支污染 | 缓存 key、miss reason、冷/热双测 |
| 兼容层 | 配置几乎不改 | 长尾插件或 runtime 行为偏差 | 官方兼容组合、双构建、浏览器回归 |
学习难度不是配置文件行数。webpack 的 Loader/Plugin 和 chunk runtime 较显式,Vite 默认更短但需要理解 dev/prod 双路径,Rollup/Rolldown 要理解 ESM、external 与输出 hooks,Babel 要理解 AST 和插件顺序。维护成本则由团队必须解释的失败模式决定:如果没人能解释 HMR dispose、cache invalidation 或 exports,短配置也会变成长事故。
迁移要保留可回滚的证据链
| 迁移 | 低风险起点 | 高风险边界 | 退出条件 |
|---|---|---|---|
| Babel→SWC/esbuild | 只删 TS 类型、常规 JSX、保留 ESM | 自定义 Babel visitor、proposal、宏、instrumentation | 所有转换 fixture、map、targets 与类型检查通过 |
| webpack→Rspack | 内建资源、常见 Loader、基础 chunks | 内部 Compiler/Compilation API、长尾 Plugin、Federation runtime | 双构建 artifact/browser、缓存和性能预算通过 |
| Rollup→Rolldown | 标准 hooks、ESM、单 output | hook 顺序状态、多 output、边缘 CJS、复杂 CSS/HTML 插件 | 消费 fixture 与产物语义通过,不要求字节相同 |
| 自研 esbuild→Vite | 保留业务源码与 public asset | HTML、env、HMR、SSR、manifest 约定 | 开发与生产双路径均通过 |
| Vite/Rollup 引擎升级 | 官方插件和显式配置 | 依赖未声明内部实现、脆弱产物快照 | ecosystem/业务回归与部署静态预览通过 |
常见错误与修正证据
| 表现 | 根因 | 先观察 | 修正 |
|---|---|---|---|
| Babel/SWC 命令成功但页面不能打开 | 只完成语法转换 | dist 是否有 HTML、CSS、依赖被解析 | 交给 bundler/dev server,不给 transformer 虚构职责 |
| dev 很快,production 白屏 | Vite dev 与 build 路径、插件 hooks 或 base 不一致 | vite build 后静态预览、Network、manifest | 同时验收 dev 和 production artifact |
| Rspack 构建成功但行为不同 | webpack Plugin/Loader 或 cache 字段只部分兼容 | 兼容组合、stats diff、插件 fixture | 换 built-in/替代插件,或保留 webpack |
| 库在本仓能 import,发布后失败 | exports、文件清单、类型或 external 错位 | npm pack 内容与独立消费者 | 对齐入口并跑 ESM/CJS/目标 bundler fixture |
| Tree Shaking 删除 CSS/注册逻辑 | 错误声明 sideEffects: false/moduleSideEffects: false | 搜索可观察 marker、浏览器行为 | 明确保留副作用路径并写回归测试 |
| async chunk 线上 404 | publicPath/base、旧 HTML 或 CDN 缓存不一致 | HTML、manifest/stats、Network URL 和响应头 | HTML 短缓存,hash assets 长缓存,原子发布 |
| HMR 后监听器翻倍 | accept/dispose 边界没有清理副作用 | 控制台计数、事件监听、timer | 在 dispose 清理;不能安全接受时 full reload |
| Source Map 指向生成代码或泄露本机路径 | 中间插件丢 map,或公开发布 external map | 解析 .map.sources、搜索工作区绝对路径 | 传递上游 map;使用 hidden map/受控上传 |
| 新工具 benchmark 更快但 CI 更慢 | 本地样例、缓存或进程模型不一致 | 原始 samples、CPU/内存、安装与启动时间 | 在真实 CI 以相同契约重测 |
一份可审查的选择记录
不要直接给工具打永久总分。为当前项目填写权重与证据:
| 约束 | 权重 | 候选证据 | 通过线 |
|---|---|---|---|
| 语义/产物可靠性 | 5 | artifact、浏览器或消费者测试 | 100% 关键路径通过 |
| 关键插件兼容 | 5 | 每个必须插件的 fixture 与官方兼容说明 | 无未解释的不兼容 |
| 冷/热性能 | 3 | 真实仓库原始样本和 CI 资源 | 达到团队 SLO |
| 开发/HMR | 3 | 固定编辑场景、状态保留、错误恢复 | 无重复副作用或长期 full reload |
| 维护与升级 | 4 | owner、版本策略、自研代码量、回滚步骤 | 团队可值班和升级 |
| 迁移成本 | 4 | 双轨周期、配置/插件/部署改动 | 能分阶段回滚 |
最终记录应写成“在 Node、浏览器、模块数、插件集、Source Map/minify 和缓存条件 X 下,候选 A 满足正确性并达到预算 Y,因此用于场景 Z;若插件 P 或运行时 R 不通过则保留候选 B”,而不是“工具 A 比工具 B 快”。
官方资料入口
- Babel:Plugins 与顺序、
preset-env、配置文件 - webpack:核心概念、缓存、Hot Module Replacement
- Rollup:配置选项、插件开发、迁移到 Rollup 4
- Vite:Why Vite、生产构建、Plugin API
- esbuild:API、Plugins、Analyze/metafile
- SWC:Compilation、Modules、Wasm plugin compatibility
- Rspack:从 webpack 迁移、Plugin compatibility、Persistent cache
- Rolldown:Introduction、Getting Started、Plugin API 差异