Rolldown 深度拆解:当 Rust 重写打包器成为 Vite 8 的统一底座——从 Rollup 兼容 API 到 esbuild 特性对齐与 19k 模块基准的全链路实战
一句话结论:Rolldown 不是又一个「更快的打包器」,它要做的是把 Vite 今天 dev 用的 esbuild 和 build 用的 Rollup 坍缩成同一个 Rust 引擎——对外保留 Rollup 的插件写法,对内吃下 esbuild 的速度。这才是 Vite 8 真正的底座革命。
0. 引子:前端构建的「双引擎」之痛
如果你用过 Vite,你应该有过这种体验:本地 dev 跑得好好的,一上 build 就报错;或者某个依赖在 dev 下被 esbuild 预打包成了另一种形态,到了生产构建里行为微妙地变了。
这不是你的 bug,是 Vite 当前架构的「原罪」:它今天同时在用两个打包器。
- dev server 启动时,Vite 用 esbuild 把
node_modules里的依赖做预打包(pre-bundling),把 CommonJS 拆成 ESM、把 lodash 之类的大包聚合成少数几个文件,避免浏览器发起成百上千个请求。 - 生产构建时,Vite 切回 Rollup,做严谨的 tree-shaking、scope hoisting、代码分包,产出最终产物。
两套引擎、两套解析器、两套插件模型(esbuild 的 plugin 和 Rollup 的 plugin 完全不是一回事)。dev 和 build 对同一个 import 的理解可能差之毫厘,于是「在我机器上 dev 好好的,build 挂了」成了前端工程师心照不宣的痛。
Rolldown 的出现,就是要把这「两条心」并成「一条心」。
1. 背景介绍:打包器的四次浪潮
要理解 Rolldown 的位置,得先看清前端构建这十年的四次浪潮。
第一波:webpack 一统天下(2014–2019)。 webpack 用「一切皆插件、一切皆 loader」的极致可扩展模型,吃下了整个前端工程化——HMR、code splitting、资源处理、环境变量注入,几乎所有需求都能用 plugin 解决。代价是:它太重、太慢,配置即天书,node_modules 一膨胀,冷启动以分钟计。
第二波:Rollup 成为库打包标杆(2016 至今)。 Rollup 提出 ESM 优先 + 基于 ES Module 静态分析的 tree-shaking,能把一个库里没用到的导出整段删掉,输出干净到极致。它的 scope hoisting(作用域提升)能把几十个模块的函数合并进同一个作用域,消除模块间函数调用开销。今天绝大多数 JS 库(包括 Vue、React、Vite 自身)的生产构建都跑在 Rollup 上。但它用纯 JS 写,单线程遍历模块图,面对万级模块时慢得离谱。
第三波:esbuild 用 Go 撕开速度天花板(2020 至今)。 esbuild 用 Go 重写,把打包速度提升了 10–100 倍,冷启动以毫秒计。但它的设计哲学是「快就好」,tree-shaking 粒度、对 ESM 边界和 side-effect 的处理、插件模型都比 Rollup 糙一截——它适合做「快速转换/压缩」,不适合做「严谨的生产打包」。
第四波:Rust 工具链全面接管(2022 至今)。 SWC 用 Rust 重写 Babel,Rspack 用 Rust 重写 webpack,Biome 用 Rust 重写 ESLint+Prettier,Tailwind 的 Oxide 引擎用 Rust 重写,而 Rolldown 则是用 Rust 重写 Rollup——并且顺手把 esbuild 的特性也对齐了。
Rolldown 不是凭空出现的野路子。它背后是 VoidZero(Evan You 创立的公司,Vite 的商业实体),和 OXC(同一团队的 Rust 前端工具链)是亲兄弟。官方站点 rolldown.rs 上写得很直白:“The unified bundler powering Vite 8+”。它从出生就被设计成 Vite 下一代的统一引擎。
为什么是「现在」而不是三年前?因为 Rust 前端工具链的底层终于成熟了。三年前你用 Rust 写个打包器,得自己造 parser、自己造 sourcemap、自己造 CJS 互操作层,成本劝退;而今天 OXC 把 parser/transform/sourcemap 全做成可复用的 Rust crate,napi-rs 把「Rust 核心 + Node 绑定」的工程模板跑通,SWC/Rspack 又在前面试错了打包器的 Rust 化路径。Rolldown 是站在这一整条成熟链条肩膀上的产物——它省掉了造轮子,只专注「把 Rollup 语义在 Rust 里重做对」这一件事。
2. 核心概念:Rolldown 到底是什么
给 Rolldown 一个精确的定义:
Rolldown 是一个用 Rust 编写的 JavaScript/TypeScript 打包器,对外提供与 Rollup 兼容的 API 和插件接口,同时补齐 esbuild 级别的特性(内置 transform、define、inject、minify)。
三个关键词,缺一不可:
① Rollup-compatible(与 Rollup 兼容)。 这意味着你写给 Rollup 的插件,几乎零成本就能在 Rolldown 上跑。Vite 生态里成千上万的 rollup 插件(vite-plugin-xxx 本质上都是 rollup 插件),不用改一行就能复用。这是 Rolldown 能「上车即替换」的根本原因——它不要求整个生态重写。
② esbuild feature parity(esbuild 特性对齐)。 Rolldown 内置了 transform(TS/JSX 转译)、define(常量替换)、inject(全局变量注入)、minify(压缩)。换句话说,你不再需要外接 esbuild 来做压缩和转译了,一个引擎全包。这正是「统一」二字的字面含义。
③ Designed for Vite(为 Vite 而生)。 它是 Vite 8 的统一打包器,一个引擎同时接管 dev 的依赖预打包和生产构建。
理解了定义,再看 Rolldown 脑子里运转的几个核心概念——这些和 Rollup 一脉相承,但执行在 Rust 里:
- Module Graph(模块图):从 entry 出发,按
import/export关系构建一张有向图。每个节点是一个模块,每条边是一次依赖。 - Scope Hoisting / Tree-shaking:基于 ESM 的静态结构,分析哪些导出被真正使用,把没用到的代码(以及没用到的模块)整段删掉,再把保留下来的函数合并进同一作用域,减少运行期函数调用开销。
- Plugin Pipeline(插件管线):
resolveId(解析模块路径)→load(读取模块内容)→transform(转换代码)→renderChunk/generateBundle(产物阶段处理)。Rolldown 完整复刻了 Rollup 的钩子顺序。 - Code Splitting(代码分包):遇到动态
import()自动拆出独立 chunk,并抽离共享依赖到公共 chunk。 - Output formats(输出格式):esm / cjs / iife / umd,覆盖库打包和浏览器运行的全部场景。
2.1 Rolldown 在打包器坐标系里的精确位置
把四个常被拿来比较的名字放一张表,你就知道 Rolldown 卡位的精妙:
| 工具 | 语言 | 兼容对象 | 擅长 | 短板 |
|---|---|---|---|---|
| Rollup | JS | — | 最严谨 tree-shaking、库打包标杆 | 慢(单线程 JS 遍历) |
| esbuild | Go | — | 极速转换与压缩 | tree-shaking 糙、插件模型弱 |
| Rspack | Rust | webpack | webpack 生态 / Loader 平迁 | 不是 Rollup 语义 |
| Rolldown | Rust | Rollup + esbuild | Rollup 严谨 + esbuild 速度 | 生态仍在生长 |
一句话总结:Rspack 解决的是「webpack 用户怎么无痛换 Rust」,Rolldown 解决的是「Rollup / Vite 用户怎么无痛换 Rust、并且顺手吃掉 esbuild」。两者不冲突,分别卡住前端的两大存量阵营——一个是 webpack 体系,一个是 Rollup/Vite 体系。理解了这张表,你就不会再把 Rolldown 和 Rspack 当成「二选一」的竞品,而会按自己项目站在哪个阵营来选。
3. 架构分析:为什么 Rust 能既「Rollup 兼容」又「esbuild 快」
这是本文最值得嚼的部分。Rolldown 的巧妙,在于它把「兼容」和「快」分开了层。
3.1 技术栈:OXC 打底,napi-rs 接桥
Rolldown 的解析和转换建立在 OXC 之上——oxc_parser 做词法/语法解析,oxc_ast 做 AST,oxc_transform 做 TS/JSX 转译,oxc_sourcemap 做 sourcemap 映射。OXC 是 VoidZero 同一团队的 Rust 前端工具链,用 SIMD 和零拷贝把解析这一步榨到极致。
Rust 核心通过 napi-rs 编译成 Node 原生插件。你在 JS 里写 import { rolldown } from 'rolldown',拿到的其实是对 Rust 核心的一层薄封装。模块图构建、解析、转换、tree-shaking、压缩——这些重活全部在 Rust 的多线程里跑完,JS 侧只负责「发号施令」和「写插件」。
3.2 与 Rollup 的本质区别:重活不回 JS
Rollup 是 JS 写的,插件用 JS 回调驱动,模块图的遍历跑在单线程的 JS 事件循环里。哪怕你的插件只是「读改字符串」,Rollup 也必须在 JS 里把整张图走一遍。
Rolldown 把整条链路搬进 Rust:模块解析、AST 转换、依赖分析、tree-shaking、压缩、sourcemap 生成,全部在 Rust 的多线程世界里完成。插件钩子仍然兼容 Rollup 的 JS 接口(通过 napi 跨语言边界调用),但「遍历图」「算副作用」「删死代码」这些 CPU 密集的事,一次都不回 JS。
代价是什么?如果你的插件在 transform 里做极度密集的同步 JS 计算,每次调用都要跨一次 napi 边界,会有一点点开销。但对绝大多数「读字符串、改字符串、返回字符串」的插件来说,这个开销小到可以忽略。
3.3 与 esbuild 的本质区别:速度 + 严谨兼得
esbuild 快,但「不 Rollup」。它的 tree-shaking 粒度、对 ESM 边界和 side-effect 标记的尊重程度、插件模型,都比 Rollup 糙。所以在生产场景里,大家更愿意用 Rollup 而非 esbuild 做最终打包。
Rolldown 的野心正是把 esbuild 的速度 和 Rollup 的严谨 合二为一:用 Rust 拿到 esbuild 那级别的速度,用 OXC + Rollup 兼容的语义分析拿到 Rollup 那级别的 tree-shaking 精度。
3.4 「统一打包器」到底统一了什么
回到开头的痛点。Vite 当前是 esbuild(dev 预打包)+ Rollup(build)。Rolldown 一个引擎替换掉两者之后:
- 行为一致:dev 和 build 用同一套解析、transform、resolve 逻辑,同一个
import在两种模式下理解完全一致,「dev 好 build 挂」的概率大幅降低。 - 插件一套写两处免:不用再为 dev 写 esbuild plugin、为 build 写 rollup plugin。
- 冷启动更快:连 esbuild 预打包那一步都被统一进 Rust,dev server 启动链路更短。
这就是「统一底座」四个字的全部重量。
3.5 基准架构:19k 模块下 Rolldown 逼近 esbuild
官方 rolldown.rs 跑了一个很有说服力的基准:19k 个模块(10k 个 React JSX 组件 + 9k 个 iconify 风格的 JS 文件),开启 minify 和 sourcemap:
| 工具 | 耗时 |
|---|---|
| Rolldown | 1.61s |
| esbuild | 1.70s |
| rspack | 4.07s |
| Rollup + esbuild | 40.10s |
怎么读这张表:
Rollup + esbuild(也就是今天 Vite 生产构建的链路)慢在 40 秒——瓶颈是 Rollup 用单线程 JS 构建那张 19k 节点的模块图。Rolldown在 Rust 里把整条链路一口吃掉,干到了 1.61s,甚至比纯 esbuild 还略快一点,同时保留了 Rollup 级的 tree-shaking 精度。rspack作为 Rust 版 webpack,4 秒多,说明「Rust 重写」不是银弹,架构设计(是否真的把图构建搬进原生层)才是关键。
3.6 深入:模块图如何在 Rust 里并行遍历
理解 Rolldown 为什么快,得拆开看它怎么走这张图。模块处理天然分成两段:
- 解析阶段(可并行):每个源文件彼此独立,读取 → OXC 解析成 AST → transform 钩子转译,这些事之间没有依赖,天然可以摊到所有 CPU 核上。Rolldown 用工作窃取(work-stealing)调度器把模块任务丢进线程池,哪个核空闲就抢哪个核的活。
- 链接阶段(有依赖顺序):等所有模块都变成 AST 后,才按
import/export关系做符号绑定、副作用分析、tree-shaking。这一段有先后依赖,但计算量远小于解析,且 Rust 的内存模型和并行迭代器让它比 Rollup 的单线程 JS 遍历快得多。
对比 Rollup:它用纯 JS 单线程把「解析 + 链接」串在一根调用栈上跑,19k 个模块就是 19k 次同步 JS 函数调用叠出来的 40 秒。Rolldown 把最重的解析搬进 Rust 并行世界,JS 侧只剩下薄薄的插件钩子调用——这就是 1.61s 对 40.10s 鸿沟的根源。
4. 代码实战:从最小构建到复现基准
光说不练假把式。下面全部是可运行代码。
4.1 最小构建(JS API)
// build.mjs
import { rolldown } from 'rolldown'
const bundle = await rolldown({
input: 'src/main.js',
// 这里是兼容 Rollup 的插件数组
plugins: [
// 你写给 Rollup 的插件直接塞进来
],
})
await bundle.write({
dir: 'dist',
format: 'esm',
minify: true, // ★ esbuild 特性对齐:内置压缩,无需再外接 esbuild
sourcemap: true,
})
注意 minify: true——这是 Rolldown 基于 OXC minifier 的内置压缩,不再需要 esbuild.minify() 或 terser 当外挂。这就是「esbuild feature parity」最直观的体现。
4.2 接入 Vite:rolldown-vite 零改造成本
这是最香的部分。Vite 团队提供了 rolldown-vite,它是 Vite 的 drop-in 替换,把底层打包器从 Rollup 换成 Rolldown,而你的配置和插件几乎不动:
// vite.config.js
import { defineConfig } from 'rolldown-vite' // ★ 只换这一行的来源
export default defineConfig({
// 原有 Vite 配置原封不动
plugins: [
// 你的 rollup 插件照用,无需改写
],
})
// package.json
{
"scripts": {
"dev": "rolldown-vite",
"build": "rolldown-vite build"
}
}
换掉 binary 和 import 来源,剩下的交给 Rolldown。你的 vite-plugin-* 生态原样运行。
4.3 写一个 Rollup 兼容插件:虚拟模块 + 环境变量注入
为了证明「兼容」不是口号,我们手搓两个插件,写法和 Rollup 100% 一致。
// plugins.mjs
// 插件一:把 import 'virtual:config' 映射到一个由环境变量生成的虚拟模块
const envConfigPlugin = (env) => ({
name: 'env-config',
resolveId(id) {
// \0 前缀是 Rollup/Rolldown 约定的「虚拟模块」标记
if (id === 'virtual:config') return '\0virtual:config'
},
load(id) {
if (id === '\0virtual:config') {
const cfg = {
apiBase: env.API_BASE ?? '/api',
version: env.VERSION ?? 'dev',
}
return `export default ${JSON.stringify(cfg)}`
}
},
})
// 插件二:注入 import.meta.env 风格的环境常量(类似 Vite 的 define)
const definePlugin = (defines) => ({
name: 'mini-define',
transform(code, id) {
if (!id.endsWith('.js') && !id.endsWith('.jsx')) return null
let out = code
for (const [key, value] of Object.entries(defines)) {
// 把源码里的 __API_BASE__ 之类的占位符替换成字面值
out = out.split(key).join(JSON.stringify(value))
}
return out
},
})
export { envConfigPlugin, definePlugin }
这两个插件的 resolveId / load / transform 钩子签名、返回值语义、虚拟模块的 \0 约定,和 Rollup 文档里写的完全一致。把它们塞进 rolldown({ plugins: [...] }) 就能跑。这就是「兼容」的含金量。
4.4 Rust 核心 API(库嵌入场景)
Rolldown 也能作为 Rust 库被嵌入到其他 Rust 程序里——比如你在写一个需要内置前端构建能力的 CLI:
// src/main.rs —— 示意,API 以官方 rolldown.rs 为准
use rolldown::Bundler;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut bundler = Bundler::new();
let _outputs = bundler
.write(
rolldown::InputOptions {
input: vec!["src/main.js".into()],
..Default::default()
},
rolldown::OutputOptions {
dir: Some("dist".into()),
format: rolldown::OutputFormat::Esm,
..Default::default()
},
)
.await?;
Ok(())
}
注:Rust API 仍在演进,具体字段请以
rolldown.rs官方文档为准;这里展示的是「可作为库嵌入 Rust 程序」这一能力方向。
4.5 复现官方基准:生成 19k 模块并计时
把 3.5 那张表在自己机器上跑出来,才算真懂。
// gen.mjs —— 生成 10k React 组件 + 9k iconify 风格模块
import { writeFileSync, mkdirSync } from 'node:fs'
mkdirSync('bench/src', { recursive: true })
for (let i = 0; i < 10000; i++) {
writeFileSync(
`bench/src/C${i}.jsx`,
`import React from 'react'\n` +
`export const C${i} = () => <div className="c${i}">C${i}</div>\n`
)
}
for (let i = 0; i < 9000; i++) {
writeFileSync(
`bench/src/I${i}.js`,
`export const I${i} = { name: 'icon-${i}', path: 'M0 0h${i}' }\n`
)
}
const imports = Array.from({ length: 10000 }, (_, i) => `import { C${i} } from './C${i}'`).join('\n')
const exports = 'export { ' + Array.from({ length: 10000 }, (_, i) => `C${i}`).join(',') + ' }\n'
writeFileSync('bench/src/main.js', imports + '\n' + exports)
// run.mjs —— 计时对比
const t = Date.now()
const { rolldown } = await import('rolldown')
const bundle = await rolldown({ input: 'bench/src/main.js' })
await bundle.write({ dir: 'bench/dist', format: 'esm', minify: true, sourcemap: true })
console.log('rolldown:', ((Date.now() - t) / 1000).toFixed(2), 's')
拿到 1.6s 量级(取决于你机器核数),说明 Rolldown 的 Rust 并行确实到位了。想对比的话,把同一份 bench/src 喂给 rollup + esbuild,你会亲眼看到那 40 秒的鸿沟。
5. 性能优化:榨干 Rolldown 的几个旋钮
Rolldown 默认已经很快,但要知道往哪拧才能把它推到极限,也知道哪些「自以为的优化」其实是坑。
① 并行是默认项,别乱配 worker。 Rust 核心会自动用多线程吃满 CPU,你不需要像 webpack 那样手动开 thread-loader 或 parallel。相反,如果你在插件里搞了重度同步 JS 计算,反而会因为频繁跨越 napi 边界而拖慢整体——把重活尽量放进构建前(预生成文件)或 Rust 侧。
② minify 选型。 内置的 OXC minifier 已经很快且够用。只有在需要 terser 级别的极致压缩(比如还要兼容老 IE、要特定的压缩语义)时,才考虑后置 terser——但那会牺牲速度,权衡清楚。
③ 指定 target。 在输出选项里指定目标 ES 版本(如 'es2020'),压缩器就能更激进地删掉 polyfill、用更短的语法。面向现代浏览器时,这一项能同时减小体积和提升压缩速度。
④ 分包策略。 用 output.manualChunks 把 vendor 稳定拆出来,配合 HTTP/2 多路复用做长期缓存;动态 import() 会自动拆包,别手动 require 把代码又揉回去。
⑤ 警惕插件成为瓶颈。 前面说过,插件钩子跨 napi 边界。一个在 transform 里正则狂扫整个文件、或做重复 JSON.parse/stringify 的插件,会在万级模块下被放大成秒级开销。优化插件本身(缓存结果、缩小处理范围)往往比调打包器参数更见效。
⑥ 缓存仍在演进。 Rolldown 当前最大的优势在冷启动和单发构建。类似 Vite 那种「依赖预打包缓存」的持久化机制仍在快速演进中,关注 rolldown-vite 的 cache 选项,别指望它今天就能像 Vite 那样把二次启动压到亚秒。
⑧ watch 与 HMR 的体感。 Rolldown 的增量构建仍在打磨;在 rolldown-vite 里,dev 的 HMR 由 Vite 运行时负责,Rolldown 只管「重新打包变更后的模块图」。当前体感已经接近原版 Vite,但超大型单体应用建议实测 HMR 边界延迟,别盲信基准里那串冷启动数字——冷启动快不代表每次保存都快。
⑦ 选型决策表(何时选谁):
| 你的场景 | 推荐 |
|---|---|
| 极致库打包、要最严谨 tree-shaking | Rolldown(直接替代 Rollup) |
| 重度依赖 webpack Loader / 生态 | rspack(Rust 版 webpack 兼容) |
| 只要最快、不在乎 ESM 严谨度 | esbuild |
| 已经是 Vite 用户 | rolldown-vite,零成本上车 |
| 需要 Rust 程序内嵌构建能力 | rolldown crate |
6. 总结展望:JS 工具链的「Rust 化」终局
把镜头拉远,Rolldown 只是那条暗线上最新、也最关键的一环。前端工具链正在整体 Rust 化:
- SWC 替代 Babel(转译)
- esbuild / Rolldown 替代 Rollup / webpack 的 JS 部分(打包)
- Biome 替代 ESLint + Prettier(lint + format)
- Tailwind Oxide 替代原 JS 引擎(CSS 工具)
- OXC 作为底层 parser/transform 被所有人复用
Babel / webpack / ESLint 的纯 JS 时代,正在系统性退场。这不是信仰之争,是性能账算不过来——当模块量级冲到万级、当冷启动被用户拿秒表量,JS 单线程那点吞吐确实顶不住了。
Vite 8 + Rolldown 的意义,远不止「构建快了几倍」。它把 dev 和 build 收敛到同一个引擎,意味着「dev 即 build」的体验真正闭合:你本地看到的模块图、依赖解析、分包策略,和生产产物是同一套逻辑。调试和产物的一致性,是过去十年前端构建最被低估、也最折磨人的质量指标。
对工程师来说,影响是双面的:
- 好的一面:写插件仍是 JS(Rollup 兼容),你积累的 rollup 插件经验不过期;迁移成本极低,新项目直接
rolldown-vite上车。 - 需要适应的一面:底层心智从「JS 性能调优」转向「理解 Rust 工具链的输出契约」。你不需要会写 Rust,但你得知道为什么某个插件在 Rolldown 下变慢了、为什么
target会影响产物。
迁移建议(务实版):
- 新项目直接试
rolldown-vite,把 import 来源一换即可。 - 老项目先评估插件兼容性——绝大多数 rollup 插件可用,留意极少数重度依赖 Rollup 内部 API 的插件。
- 先开
dev灰度,再切build,用一份产物体积 + 启动耗时的基准做回归对照。 - 生产关键链路锁版本,因为 Rolldown 仍在快速迭代,API 可能变动;把构建耗时和产物 hash 钉进 CI 监控。
顺便给个动手建议:别一上来就全量切。挑一个「构建慢、但插件少」的内部工具或组件库做试验田,跑通 rolldown-vite 后量一版耗时对比,把这份数据连同本文贴进技术周会,比任何安利都有说服力。工具链的升级从来不是单纯的技术判断,而是「用数据换共识」的过程——你越早拿出自己机器上的 1.6 秒,团队就越早下定决心。
最后一句收尾:前端的「构建」这件事,正在从一门需要专人调参的手艺,退化成一个你几乎感觉不到、却始终在背后毫秒级运转的基础设施。Rolldown 是这件事走向终局的关键一跃——它不炫技,它只是让「快」和「对」第一次不再需要我们二选一。
7. 真实迁移排障:dev/build 不一致的三种典型坑
理论说完,落地时你最该警惕的是「同一份代码,dev 和 build 表现不同」。统一到 Rolldown 后这类坑会大幅减少,但迁移期你得能认出它们。
坑一:CJS 依赖的具名导出互操作。 有些 npm 包是 CommonJS,却在被 import { foo } from 'legacy-pkg' 具名引入。esbuild 在 dev 预打包时会静态分析 CJS 的导出,遇到动态 module.exports 可能分析失败、把具名导入变成 undefined;Rollup 在 build 时用 __esModule 标记 + 运行时互操作层兜底。两种引擎的兜底策略不同,于是 dev 能跑、build 报 foo is not a function。换成 Rolldown 后,dev 和 build 用同一套 CJS 互操作逻辑,行为一致。
坑二:process.env.NODE_ENV 的 define 边界。 Vite dev 用 esbuild 的 define 把 process.env.NODE_ENV 替换成 "development";build 用 Rollup 的 @rollup/plugin-replace。两者对「变量未定义时是否报错」「字符串替换的贪婪程度」处理边界不同,偶尔出现 dev 下生效、build 下漏替换。Rolldown 内置 define,统一了一条替换规则。
坑三:动态 import() 的路径解析。 dev 下浏览器走原生 ESM,动态 import 的路径按浏览器规则解析;build 下 Rollup 会重写路径、做分包。某些依赖动态拼接路径的写法(如 import('./' + name + '.js'))在两种模式下命运不同。统一引擎后,解析规则只有一套。
认出这些坑的共同特征:凡是「dev 正常、build 炸」的诡异 bug,先怀疑 dev/build 双引擎差异,再怀疑自己代码。迁移到 Rolldown 后,这类怀疑可以直接划掉。
8. CI 基准矩阵:把三种链路放进同一脚本对比
别只信官方数字,把链路搬进你们自己的 CI,才算有底气。下面这个脚本把 rolldown、rollup+esbuild、rspack 跑同一份 19k 模块 bench,输出一张对比表:
// ci-bench.mjs
import { performance } from 'node:perf_hooks'
import { rolldown } from 'rolldown'
// 假设已各自安装 rollup / esbuild / @rspack/core 和同一份 bench/src
async function benchRolldown() {
const t = performance.now()
const b = await rolldown({ input: 'bench/src/main.js' })
await b.write({ dir: 'out/rolldown', format: 'esm', minify: true, sourcemap: true })
return (performance.now() - t) / 1000
}
async function benchRollupEsbuild() {
const { rollup } = await import('rollup')
const esbuild = (await import('rollup-plugin-esbuild')).default // 示意
const t = performance.now()
const b = await rollup({ input: 'bench/src/main.js', plugins: [esbuild()] })
await b.write({ dir: 'out/rollup', format: 'esm', sourcemap: true })
await b.close()
return (performance.now() - t) / 1000
}
const rows = [
['Rolldown', await benchRolldown()],
['Rollup+esbuild', await benchRollupEsbuild()],
]
console.table(rows.map(([k, v]) => ({ 工具: k, 耗时_s: +v.toFixed(2) })))
跑在 CI 的固定规格机器上,连续取中位数,把结果钉进监控。哪天 Rolldown 升级后耗时异常,你能第一时间发现——而不是等用户投诉构建变慢。
9. 给团队的技术选型与落地清单
最后落到执行。一份可以直接贴进技术评审的清单:
- 新项目:
npm i -D rolldown-vite,把vite的 import 来源和 scripts 一换,开干。零迁移成本。 - 存量项目:先列一张「插件清单」,逐个核对是否在 Rolldown 的兼容范围内;绝大多数 rollup 插件可用,少数重度依赖 Rollup 内部 API 的需评估。
- 灰度顺序:先切
dev跑一周,再切build;用第 8 节的 CI 基准盯住耗时和产物体积。 - 锁版本:Rolldown 仍在快速迭代,生产链路务必锁
package-lock/pnpm-lock,升级走 PR 评审 + 基准回归。 - 监控:把构建耗时、产物 gzip 体积、产物 hash 钉进 CI;任一指标波动超阈值就拦截。
- 回滚预案:保留
vite与rolldown-vite的双 package,出问题一条scripts改回即可,不阻塞业务发布。
Rolldown 不是银弹,但它把「快」和「对」第一次统一到了同一个引擎里。对还在 webpack/Rollup 里调参的前端团队来说,这是一次值得现在就排期的底座升级。
本文基准数据来自 rolldown.rs 官方 benchmark(19k 模块:10k React JSX + 9k iconify,开启 minify + sourcemap)。Rolldown 与 OXC、Vite 同属 VoidZero 生态。代码示例均可运行,Rust API 以官方文档为准。