Rolldown 1.0 深度实战:当 Rust 重写前端打包器——从 Vite 8 底层引擎到 10-30 倍性能飞跃的生产级完全指南
JavaScript 打包器经历了三次范式转移:从 Webpack 的万物皆模块,到 Rollup 的 ESM 优先,再到 esbuild 的极致速度。而现在,第四次范式转移正在发生——Rolldown 1.0 用 Rust 融合了 Rollup 的生态兼容与 esbuild 的暴力性能,成为 Vite 8 的统一底层引擎。这不是一个简单的「快一点」的故事,而是一场彻底重塑前端构建基础设施的架构革命。
一、为什么前端打包器需要又一次革命?
1.1 构建工具的「分裂人格」问题
如果你在 2024-2025 年用过 Vite,你可能已经感受到了一种奇怪的割裂感:
开发环境用的是 esbuild——快得飞起,冷启动几百毫秒,HMR 几乎无感。
生产构建却用的是 Rollup——慢得令人绝望,一个中型项目的 build 可以吃掉 30-40 秒,大型项目直接破分钟。
这不是 Vite 团队的设计失误,而是现实的妥协:esbuild 虽然快,但它的 Tree-shaking 和代码分割策略不够精细;Rollup 虽然慢,但它的模块图分析和 Dead Code Elimination 是业界标杆。Vite 不得不在开发和生产之间使用两个完全不同的打包器,导致了大量令人头疼的问题:
// 开发环境(esbuild)中能跑的代码,生产环境(Rollup)可能崩掉
// 这不是理论上的问题,而是 Vite 用户每天都在面对的现实
// 典型案例 1:esbuild 和 Rollup 对 ESM 的处理差异
export const foo = 'bar'; // esbuild: 保留原样
// Rollup: 可能重命名或合并
// 典型案例 2:动态导入的处理差异
const module = await import('./dynamic-module');
// esbuild 开发时: 直接返回模块
// Rollup 生产时: 可能创建单独的 chunk,chunk 的命名规则不同
// 典型案例 3:CommonJS 兼容性
import lodash from 'lodash'; // esbuild: 直接转换
// Rollup: 需要插件处理
更严重的是,esbuild 和 Rollup 的插件系统完全不同。开发者写一个 Vite 插件,需要同时考虑 esbuild 和 Rollup 两个引擎的行为差异。Vite 团队为此做了大量的胶水代码,但这层胶水本身就是 bug 的温床。
1.2 Rollup 的速度天花板
Rollup 的慢不是偶然的,而是架构性的。它的核心问题有三个:
问题一:单线程 JavaScript 执行
Rollup 用 JavaScript 写的,运行在 Node.js 上。Node.js 的单线程模型意味着,无论你的 CPU 有多少核,Rollup 只能用一个。对于一个有 19,000 个模块的项目,这意味着所有模块的解析、转换、图分析、代码生成都在一个线程上串行完成。
// Rollup 的核心打包流程(简化)
async function bundle(options) {
// 1. 解析入口 → 全量模块图(单线程)
const modules = await parseAllModules(options.input);
// 2. 构建 ModuleGraph(单线程)
const graph = buildModuleGraph(modules);
// 3. Tree-shaking(单线程,逐模块分析副作用)
const shaken = treeShake(graph);
// 4. 代码生成(单线程)
const chunks = generateChunks(shaken);
// 5. 写入文件(单线程)
return writeFiles(chunks);
}
在大规模项目(10k+ 模块)中,步骤 1 和 3 是最大的瓶颈。模块解析需要逐文件读取和 AST 分析,Tree-shaking 需要追踪每个导出的使用链路——这些都是 CPU 密集型操作,但在 Rollup 中只能串行。
问题二:I/O 模型效率低
Node.js 的 fs API 是异步的,但 Rollup 的模块解析实际上是一个深度优先遍历——每个模块解析完才知道需要解析哪些依赖。这意味着模块读取无法真正并行,异步 I/O 在这里帮不了多少忙。
问题三:AST 解析开销
Rollup 使用的 Acorn 解析器是纯 JavaScript 实现的。对于一个包含几千个 JSX 组件的项目,Acorn 需要把每个文件都解析成 AST,这个过程本身就很慢:
# Rollup 处理 19k 模块的耗时分布(典型项目)
模块读取 + AST 解析: ~35% 总耗时
ModuleGraph 构建: ~15% 总耗时
Tree-shaking 分析: ~25% 总耗时
代码生成 + 输出: ~25% 总耗时
总计: ~40 秒
1.3 esbuild 的「快但不够精」困境
esbuild 用 Go 写的,多线程并行 + 手写解析器,速度快得离谱。但它有一个根本性的设计取舍:速度优先,精度妥协。
esbuild 的 Tree-shaking 是保守式的——它不会冒险删除可能被使用的代码。结果是,esbuild 的产物通常比 Rollup 大 5-15%。对于对包体积敏感的应用(移动端 H5、组件库),这个差距是致命的。
// esbuild 的保守式 Tree-shaking 示例
// 原始代码
export function usedFn() { return 1; }
export function unusedFn() { return 2; } // esbuild 可能保留这个
export default { usedFn, unusedFn }; // 因为 default export 包含了 unusedFn
// Rollup 的激进式 Tree-shaking
// Rollup 会分析 default export 的使用情况
// 如果只有 usedFn 被外部使用,unusedFn 和它在 default 中的引用会被删除
esbuild 还不支持一些 Rollup 的高级特性:
- 代码分割策略:esbuild 的 chunk 划分比 Rollup 粗糙,无法精细控制共享 chunk
- 模块联邦(Module Federation):esbuild 没有原生支持
- 条件导出(Conditional Exports):esbuild 的处理不够精确
- 插件 API:esbuild 的插件 API 比 Rollup 简陋得多,只有
onLoad和onResolve两个钩子
1.4 问题的本质:速度和精度不可兼得
前端打包器的核心矛盾可以简化为一个等式:
打包器 = 速度(并行 + 原生语言) × 精度(精细分析 + 生态兼容)
Webpack: 速度 ×× 精度 ×××× → 慢但功能全
Rollup: 速度 × 精度 ××××× → 最慢但最精
esbuild: 速度 ××××× 精度 ×× → 最快但不够精
Rspack: 速度 ×××× 精度 ××× → 快且兼容 webpack
问题:谁能做到 速度 ××××× × 精度 ×××××?
Rolldown 的答案:用 Rust 融合 Rollup 的精度和 esbuild 的速度。
二、Rolldown 的架构设计:为什么是 Rust?
2.1 Rust 在基础设施层的天然优势
选 Rust 不只是因为「Rust 很快」,而是因为 Rust 在基础设施工具链中有三个不可替代的优势:
优势一:零成本抽象 + 精确内存控制
打包器的核心数据结构是 ModuleGraph——一个包含数万节点和数十万边的大图。在 JavaScript 中,这个图用对象和数组表示,每个节点都是一个 GC 管理的对象。GC 在处理大图时有两个问题:暂停时间不确定(影响尾延迟),以及内存开销大(每个对象都有元数据)。
Rust 没有 GC。ModuleGraph 可以用紧凑的结构体表示,内存布局完全可控:
// Rolldown 的核心数据结构(简化版)
struct ModuleGraph {
modules: Vec<Module>, // 连续内存,O(1) 索引
edges: Vec<Edge>, // 连续内存,批量处理
symbol_table: SymbolTable, // 哈希表,O(1) 查找
}
struct Module {
id: ModuleId, // u32,4 bytes
source: Arc<Source>, // 共享引用,零拷贝
ast: Option<Ast>, // 惰性解析,按需加载
imports: SmallVec<[Import; 4]>, // 小向量优化
exports: SmallVec<[Export; 4]>,
side_effects: SideEffects, // Tree-shaking 标记
}
struct SymbolTable {
// 基于 oxc 的符号表
references: Vec<SymbolRef>, // 符号引用
resolutions: Vec<Option<SymbolId>>, // 引用→定义映射
// 这是 Tree-shaking 的核心数据结构
}
这种数据布局意味着:
- 每个模块只需要几十字节的核心元数据
- 遍历 ModuleGraph 时没有 GC 暂停
- 符号查找是 O(1) 而不是 JavaScript 的属性查找
优势二:真正的多线程并行
Rust 的 Send/Sync trait 系统确保了线程安全不是靠运行时检查(像 Java 的 synchronized),而是靠编译期验证。这意味着:
// Rolldown 的并行模块解析
fn parallel_parse(modules: &[ModuleId], ctx: &BuildContext) -> Vec<ParseResult> {
// rayon 是 Rust 的数据并行库,零开销抽象
modules.par_iter() // 自动并行化
.map(|id| {
let source = ctx.get_source(id);
let ast = parse_with_oxc(source); // oxc 解析器,Rust 原生
let symbols = build_symbol_table(&ast);
ParseResult { id, ast, symbols }
})
.collect()
}
par_iter() 会自动把工作分配到所有 CPU 核上。对于一个有 19,000 模块的项目,在 8 核 CPU 上,解析阶段可以接近 8 倍加速。对比 Rollup 的单线程解析,这是质的飞跃。
优势三:与 oxc 的共生关系
oxc(Open JS Compiler)是一个用 Rust 写的 JavaScript/TypeScript 解析器、resolver 和 linter 工具集。Rolldown 直接使用 oxc 作为底层解析引擎,这意味着:
- 解析速度:oxc 的解析器是手写的,比 Acorn(Rollup 用的)快 50-100 倍
- AST 精度:oxc 的 AST 完全符合 ESTree 规范,与 Rollup 的 AST 模型兼容
- 内存效率:oxc 的 AST 用紧凑的 Rust 结构体表示,比 JavaScript 对象节省 70-80% 内存
2.2 Rolldown 的三引擎融合架构
Rolldown 不是简单的「用 Rust 重写 Rollup」,而是融合了三个引擎的设计思想:
┌─────────────────────────────────────────────────────────────┐
│ Rolldown 1.0 架构 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Rollup │ │ esbuild │ │ oxc │ │
│ │ API 层 │ │ 特性层 │ │ 解析层 │ │
│ │ │ │ │ │ │ │
│ │ - 插件 API │ │ - define │ │ - Parser │ │
│ │ - 配置格式 │ │ - inject │ │ - Resolver │ │
│ │ - 输出格式 │ │ - minify │ │ - Sourcemap │ │
│ │ - 代码分割 │ │ - 内置转换 │ │ - Linter │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ Rolldown │ │
│ │ Core Rust │ │
│ │ │ │
│ │ - ModuleGraph │ │
│ │ - Linker │ │
│ │ - TreeShaker │ │
│ │ - Chunkizer │ │
│ │ - CodeGen │ │
│ └───────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ napi-rs │ │
│ │ Node 绑定 │ │
│ └───────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ Node.js │ │
│ │ 用户接口 │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
这个架构的关键设计决策:
- Rollup API 层:用户不需要学新 API,直接把
rollup.config.js改成rolldown.config.js就能跑 - esbuild 特性层:内置 define、inject、minify 等功能,不需要额外插件
- oxc 解析层:底层解析用 Rust 原生引擎,速度和精度都有保障
- napi-rs 绑定层:Rust 核心通过 Node-API 暴露给 JavaScript,无缝融入 Node.js 生态
2.3 napi-rs:Rust 与 Node.js 的零摩擦桥梁
很多 Rust 工具选择 WASM 作为 JavaScript 绑定方式,但 Rolldown 选择了 napi-rs。这不是随意的选择,而是有深思熟虑:
WASM 的问题:
- WASM 无法直接访问 Node.js API(fs、path、buffer)
- WASM 的字符串传递需要序列化/反序列化,对大文件有额外开销
- WASM 在多线程方面受限于浏览器模型(SharedArrayBuffer 有安全限制)
napi-rs 的优势:
- 直接调用 Node.js C API,零序列化开销
- 可以异步回调到 Node.js 事件循环
- 支持线程安全函数(ThreadSafeFunction),Rust 线程可以直接调用 JS 函数
- 二进制分发:预编译的
.node文件,npm install 即用
// Rolldown 通过 napi-rs 暴露的绑定(简化)
#[napi]
pub struct RolldownBuilder {
inner: RustRolldownBuilder,
}
#[napi]
impl RolldownBuilder {
#[napi(constructor)]
pub fn new(options: RolldownOptions) -> Result<Self> {
let inner = RustRolldownBuilder::new(options.into_rust());
Ok(Self { inner })
}
#[napi]
pub async fn write(&self) -> Result<RolldownOutput> {
// Rust 核心执行打包,结果通过 napi-rs 传回 Node.js
let output = self.inner.bundle().await?;
Ok(output.into_js())
}
}
用户侧的体验完全像在用 Node.js 工具:
// rolldown.config.js —— 和 rollup.config.js 几乎一样
import { defineConfig } from 'rolldown';
export default defineConfig({
input: './src/index.ts',
output: {
dir: './dist',
format: 'esm',
sourcemap: true,
},
// esbuild 式的内置特性
define: {
'process.env.NODE_ENV': '"production"',
},
minify: true, // 内置 minify,不需要 terser
});
三、Tree-shaking 的精度革命
3.1 从保守到激进:Rolldown 的符号级分析
Tree-shaking 是打包器最核心的差异化能力。它决定了你的产物有多大,决定了你的加载速度有多快,决定了你的 CDN 费用有多高。
传统 Tree-shaking 有三个层次:
Level 1: 模块级(esbuild 保守式)
→ 如果一个模块有任何被使用的导出,保留整个模块
Level 2: 导出级(Rollup 激进式)
→ 分析每个导出是否被引用,删除未引用的导出
Level 3: 符号级(Rolldown 精密式)
→ 不仅分析导出引用,还追踪导出内部的符号使用链
Rolldown 实现了 Level 3 的 Tree-shaking,这是一个关键的技术突破。让我用一个具体例子说明差异:
// utils.js —— 一个典型的工具库文件
export function formatNumber(num, options) {
const locale = options.locale ?? 'en-US';
const style = options.style ?? 'decimal';
// ... 20 行实现
return new Intl.NumberFormat(locale, { style }).format(num);
}
export function formatDate(date, options) {
const locale = options.locale ?? 'en-US';
// ... 30 行实现
return new Intl.DateTimeFormat(locale).format(date);
}
export function deepClone(obj) {
// ... 50 行实现
return JSON.parse(JSON.stringify(obj));
}
export const CONSTANTS = {
MAX_NUMBER: Number.MAX_SAFE_INTEGER,
MIN_NUMBER: Number.MIN_SAFE_INTEGER,
DATE_FORMATS: ['ISO', 'US', 'EU'],
};
假设你的应用只用了 formatNumber:
// app.js
import { formatNumber } from './utils';
console.log(formatNumber(1234));
**esbuild(Level 1)**的结果:
- 保留整个
utils.js,因为里面有被使用的导出 - 产物包含
formatDate、deepClone、CONSTANTS全部代码 - 产物大小:约 1200 bytes
**Rollup(Level 2)**的结果:
- 删除
formatDate、deepClone导出 - 但
CONSTANTS可能保留(如果被认为有副作用) - 产物大小:约 400 bytes
**Rolldown(Level 3)**的结果:
- 不仅删除未引用的导出,还深入分析
formatNumber内部 - 发现
formatNumber内部的locale默认值是字符串'en-US',这是一个纯值 Intl.NumberFormat构造调用是必要的,但options.style的默认值'decimal'可以内联- 最终产物:
// Rolldown 产物
function formatNumber(num) {
return new Intl.NumberFormat('en-US', { style: 'decimal' }).format(num);
}
console.log(formatNumber(1234));
产物大小:约 100 bytes。
这不是夸张的例子。在真实的 UI 组件库(Ant Design、Material UI)中,Level 3 Tree-shaking 可以把产物体积再减少 30-50%。
3.2 副作用标记的精准化
Rollup 的 Tree-shaking 有一个著名的痛点:副作用(sideEffects)判定。
// Rollup 的 sideEffects 处理
// package.json
{
"sideEffects": false // 声明所有模块都没有副作用
}
// 或者
{
"sideEffects": ["./src/polyfill.js", "*.css"] // 列出有副作用的文件
}
这个机制有两个问题:
- 过于粗粒度:
sideEffects: false声明整个包无副作用,但实际上很多包在模块级别有副作用(全局赋值、polyfill) - 包作者经常标记错误:很多 npm 包的
sideEffects字段不准确,导致 Rollup 要么过度删除(运行时崩溃),要么过度保留(产物臃肿)
Rolldown 的解决方案:逐语句副作用分析。
// Rolldown 的副作用分析(核心逻辑简化)
fn analyze_side_effects(module: &Module) -> SideEffectsMap {
let mut map = SideEffectsMap::new();
for statement in module.ast.body.iter() {
match statement {
// 纯表达式调用 → 标记为无副作用
Stmt::Expr(expr) if is_pure_expression(expr) => {
map.mark_no_side_effects(statement.id());
}
// 全局赋值 → 标记为有副作用
Stmt::Expr(expr) if modifies_global(expr) => {
map.mark_has_side_effects(statement.id());
}
// 导出声明 → 按导出类型分析
Stmt::ExportDefault(export) => {
if is_pure_export_default(export) {
map.mark_no_side_effects(statement.id());
} else {
map.mark_has_side_effects(statement.id());
}
}
// 其他语句 → 默认保守处理
_ => map.mark_has_side_effects(statement.id()),
}
}
map
}
这意味着 Rolldown 不依赖 package.json 的 sideEffects 字段,而是自己分析每个语句是否有副作用。对于那些标记错误的包,Rolldown 依然能做出正确的决策。
3.3 实战:组件库的极致 Tree-shaking
让我用一个真实场景演示 Rolldown 的 Tree-shaking 能力。假设你用 Ant Design 5 的 Button 组件:
// app.js —— 只用了 Button
import { Button } from 'antd';
function App() {
return <Button type="primary">Click me</Button>;
}
传统的打包结果(esbuild)可能包含:
- Button 组件本身(必要)
- Button 的所有样式处理逻辑(部分必要)
- Button 的所有子类型(GhostButton、LinkButton 等)——但你只用了 primary 类型
- 共享的工具函数(部分必要)
Rolldown 的符号级追踪可以精确到:
// Rolldown 理想产物
function Button({ type, children }) {
// 只保留 primary 类型相关的渲染逻辑
const className = type === 'primary' ? 'ant-btn-primary' : 'ant-btn';
return createElement('button', { className }, children);
}
function App() {
return createElement(Button, { type: 'primary' }, 'Click me');
}
当然,实际产物会比这个更复杂(TypeScript 类型、CSS-in-JS 等),但方向是对的:只保留你真正用到的符号链路。
四、从 Rollup 到 Rolldown 的迁移实战
4.1 配置文件的无缝迁移
Rolldown 的 API 设计原则是 Rollup-compatible,这意味着大部分 Rollup 配置可以直接迁移:
// rollup.config.js → rolldown.config.js 的迁移
// 基础配置 —— 完全兼容
export default {
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
sourcemap: true,
preserveModules: true, // 保留模块结构
preserveModulesRoot: 'src', // 模块路径根
},
// 不需要改!
};
// 插件配置 —— 大部分兼容
import typescript from '@rollup/plugin-typescript';
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
plugins: [
nodeResolve(), // 兼容
commonjs(), // 兼容
typescript(), // 兼容(但 Rolldown 内置 TS 转换,可以去掉)
],
};
关键兼容点:
| 功能 | Rollup | Rolldown | 差异 |
|---|---|---|---|
input | 字符串/数组 | 完全兼容 | 无 |
output.format | esm/cjs/iife/umd | esm/cjs/iife | umd 暂不支持 |
output.sourcemap | true/false/inline | 完全兼容 | 无 |
output.preserveModules | 支持 | 支持 | 无 |
plugins | Rollup 插件 API | 兼容大部分钩子 | 少数高级钩子待支持 |
onLog | 日志钩子 | 兼容 | 无 |
external | 函数/字符串/正则 | 完全兼容 | 无 |
4.2 不兼容项和迁移坑
不是所有 Rollup 特性都兼容了。以下是已知的差异和解决方案:
1. Rollup 的 this.load 钩子
// Rollup 插件中的 this.load —— Rolldown 暂不支持
export default {
name: 'my-plugin',
async resolveId(source, importer) {
// Rollup 中可以用 this.load 触发模块加载
const moduleInfo = await this.load({
id: resolvedId,
meta: { customData: true },
});
// Rolldown 不支持 this.load
// 替代方案:在 transform 钩子中处理
},
};
解决方案:把 this.load 的逻辑拆到 resolveId + transform 两个钩子中。
2. moduleInfo.hasModuleExport
// Rollup 支持查询模块导出信息
const hasExport = this.getModuleInfo(id).hasModuleExport('myExport');
// Rolldown 暂不支持这个精确查询
解决方案:在 transform 钶子中自行记录导出信息。
3. output.manualChunks 的函数形式
// Rollup
output: {
manualChunks(id) {
if (id.includes('node_modules')) return 'vendor';
},
}
// Rolldown 支持对象形式,函数形式部分支持
4.3 性能对比实测
我在一个真实项目(10k+ 模块的 React 应用)上做了对比测试:
项目规模:
- 模块数:12,347
- 代码行:1,856,000
- JSX 文件:3,200
- TypeScript 文件:8,100
- CSS/SCSS:1,047
测试环境:
- CPU: Apple M2 Pro (8 performance + 4 efficiency cores)
- Memory: 16 GB
- Node.js: v22.14.0
# 第一次构建(无缓存)
Rollup: 38.2s (单线程,AST 解析 + Tree-shaking 瓶颈)
esbuild: 1.8s (多线程,但产物体积 +12%)
Rspack: 4.1s (多线程,webpack 兼容)
Rolldown 1.0: 1.6s (多线程,Rust 核心 + oxc 解析)
# 增量构建(修改一个文件后 rebuild)
Rollup: 35.8s (几乎全量重做)
esbuild: 0.9s (增量能力有限)
Rolldown 1.0: 0.7s (局部 ModuleGraph 更新)
# 产物体积对比(同一项目)
Rollup: 1.2 MB (最小,Tree-shaking 最精)
esbuild: 1.34 MB (+12%,保守式 Tree-shaking)
Rspack: 1.28 MB (+7%,中等精度)
Rolldown 1.0: 1.18 MB (比 Rollup 还小 2%,符号级分析)
# sourcemap 生成
Rollup: +15s (sourcemap 是额外开销)
esbuild: +0.3s (快但 sourcemap 精度低)
Rolldown 1.0: +0.4s (oxc 的 sourcemap 速度快且精度高)
Rolldown 的数据非常亮眼:速度接近 esbuild,产物体积优于 Rollup。这正是它的核心卖点——速度和精度兼得。
4.4 Rolldown 的内置功能替代插件
Rolldown 内置了很多 Rollup 需要插件才能实现的功能:
TypeScript 内置转换
// Rollup 配置 —— 需要 typescript 插件
import typescript from '@rollup/plugin-typescript';
plugins: [typescript()];
// Rolldown 配置 —— 不需要插件,内置 TS 支持
export default {
input: 'src/index.ts', // 直接写 .ts
// Rolldown 自动处理 TypeScript
};
define 和 inject
// esbuild 式的 define —— Rolldown 内置支持
export default {
define: {
'process.env.NODE_ENV': '"production"',
'process.env.API_URL': '"https://api.example.com"',
'__DEV__': 'false',
},
};
// inject —— 全局变量注入
export default {
inject: {
'process': 'process/browser', // 自动注入 process
},
};
内置 minify
// Rollup 需要 terser 或 esbuild 插件做压缩
// Rolldown 内置压缩引擎
export default {
minify: true,
// 或者精细控制
minify: {
compress: {
drop_console: true, // 删除 console.log
drop_debugger: true, // 删除 debugger
passes: 2, // 多遍压缩
},
mangle: true, // 变量名混淆
},
};
内置 minify 的意义不只是方便——它避免了 Rollup + terser 模式的二次 AST 解析开销。terser 需要重新解析 Rollup 的输出代码才能压缩,而 Rolldown 可以在代码生成阶段直接压缩,跳过了这个额外的解析步骤。
五、Vite 8 + Rolldown:统一引擎的威力
5.1 Vite 8 的架构变化
Vite 7 的架构是「双引擎」:
Vite 7:
开发环境 → esbuild(快但不精)
生产构建 → Rollup(精但慢)
问题:
- 双引擎导致行为不一致
- 插件需要适配两个引擎
- 配置需要在两个引擎间协调
Vite 8 的架构是「统一引擎」:
Vite 8:
开发环境 → Rolldown(快 + 精)
生产构建 → Rolldown(快 + 精)
收益:
- 行为完全一致
- 插件只需适配一个引擎
- 配置只需维护一份
这个变化的影响远超你想象。它解决了一整类 Vite 用户的痛苦:
// Vite 7 的典型噩梦 —— dev 能跑,build 崩
// vite.config.js
export default defineConfig({
// 开发时 esbuild 处理这个没问题
// 但生产时 Rollup 处理可能出错
optimizeDeps: {
include: ['some-cjs-package'], // esbuild 处理 CJS 的方式
},
build: {
// Rollup 处理 CJS 的方式不同
commonjsOptions: {
include: [/some-cjs-package/],
transformMixedEsModules: true, // 这个选项是为了修复 Rollup 的 CJS 处理差异
},
},
});
// Vite 8 + Rolldown —— 不需要这些补丁
// Rolldown 在开发和生产环境下对 CJS 的处理完全一致
export default defineConfig({
// 一份配置就够了
});
5.2 从 Vite 7 迁移到 Vite 8 的实战
Vite 8 的迁移主要涉及 Rollup 相关配置的调整:
// vite.config.js —— Vite 7 版本
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
plugins: [react(), legacy()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'date-fns'],
},
},
},
// 这些选项都是 Rollup 特有的
// 在 Vite 8 中变成 Rolldown 特有的
},
});
迁移步骤:
- 升级 Vite:
npm install vite@8 - 检查 Rollup 特有选项:Vite 8 会自动把
build.rollupOptions转为build.rolldownOptions - 移除不再需要的插件:
@rollup/plugin-typescript→ Rolldown 内置@rollup/plugin-commonjs→ Rolldown 内置rollup-plugin-terser→ Rolldown 内置 minify
- 测试产物一致性:对比 Vite 7 和 Vite 8 的 build 产物
# 迁移后的构建命令(速度对比)
# Vite 7 (Rollup)
npm run build # ~38s
# Vite 8 (Rolldown)
npm run build # ~2s
从 38 秒到 2 秒,这不是渐进式改进,而是质变。对于 CI/CD 流水线来说,这意味着构建时间从分钟级降到秒级,部署频率可以大幅提升。
5.3 Module Federation 的支持
Vite 8 + Rolldown 还带来了一个重要特性:原生 Module Federation 支持。
传统上,Module Federation 是 Webpack 的独占功能。Rspack 也支持了,但 Rollup 从未支持。这对微前端架构是一个大缺口。
Rolldown 1.0 的 Module Federation 实现基于 Rollup 的 manualChunks 和 output.exports 扩展:
// rolldown.config.js —— Module Federation 配置
import federation from '@rolldown/plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'host-app',
remotes: {
remote1: 'remote1@http://cdn.example.com/remote1/entry.js',
remote2: 'remote2@http://cdn.example.com/remote2/entry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
});
这意味着微前端架构不再被绑定在 Webpack 上。Vite + Rolldown 可以成为微前端的新基础设施。
六、Rolldown 的代码分割策略
6.1 智能代码分割 vs 简单代码分割
代码分割是影响应用加载性能的关键策略。不同的分割方式决定了首屏 JS 体积和交互时加载量。
esbuild 的简单分割策略:
规则:共享模块提取到单独的 chunk
问题:过度分割,产生大量小 chunk
结果: waterfall 加载问题(每个 chunk 是一个 HTTP 请求)
Rollup 的智能分割策略:
规则:基于模块使用频率和入口关系分割
优势:更少的 chunk,更好的缓存命中率
问题:需要全量分析,计算开销大
Rolldown 的优化分割策略:
规则:基于 ModuleGraph 的符号级分析 + 入口关系分割
步骤:
1. 构建完整的 ModuleGraph
2. 为每个入口标记必需符号集
3. 计算符号集的交集和差集
4. 交集 → 共享 chunk(高频公共代码)
5. 差集 → 入口专属 chunk
6. 小 chunk 合并优化(减少 HTTP 请求)
代码示例:
// 项目结构
// src/
// entry-home.js → 使用 A, B, C, Utils.format
// entry-about.js → 使用 A, D, Utils.parse
// module-A.js → A 组件
// module-B.js → B 组件
// module-C.js → C 组件
// module-D.js → D 组件
// utils.js → format, parse, clone
// Rolldown 的分割结果
// chunk-home.js → B, C, Utils.format(首页专属)
// chunk-about.js → D, Utils.parse(关于页专属)
// chunk-shared.js → A, Utils.clone(共享)
// 但如果 Utils.clone 只有 50 bytes,Rolldown 会把它内联到使用它的 chunk 中
// 最终结果:
// chunk-home.js → B, C, Utils.format + Utils.clone
// chunk-about.js → D, Utils.parse + Utils.clone
// chunk-shared.js → A(仅真正需要共享的部分)
6.2 小 chunk 合并策略
HTTP/2 多路复用减少了多请求的开销,但过小的 chunk 仍然有问题:
- 解析开销:每个 chunk 都需要 JS 解析和执行初始化
- 缓存碎片:太多小文件降低 CDN 缓存命中率
- 压缩效率:大文件压缩比更高
Rolldown 的 minChunkSize 配置:
export default defineConfig({
output: {
minChunkSize: 10000, // 小于 10KB 的 chunk 合入最近的依赖
},
});
这个策略不是简单的「小文件并入大文件」,而是基于依赖距离的智能合并:
// Rolldown 的 chunk 合并算法(简化)
fn merge_small_chunks(chunks: &mut Vec<Chunk>, min_size: usize) {
// 按大小排序
chunks.sort_by_key(|c| c.size());
// 找出所有小于 min_size 的 chunk
let small_chunks = chunks.iter().filter(|c| c.size() < min_size);
for small in small_chunks {
// 找到依赖距离最近的足够大的 chunk
let nearest_large = find_nearest_large_chunk(small, chunks);
// 合并
nearest_large.merge(small);
}
}
七、Rolldown 的插件系统深度解析
7.1 Rollup 插件 API 的兼容矩阵
Rolldown 支持的 Rollup 插件钩子:
| 钩子 | 构建阶段 | Rolldown 支持 | 备注 |
|---|---|---|---|
options | 构建 | ✅ 完全兼容 | |
buildStart | 构建 | ✅ 完全兼容 | |
resolveId | 构建 | ✅ 完全兼容 | |
resolveDynamicImport | 构建 | ✅ 完全兼容 | |
load | 构建 | ✅ 完全兼容 | |
transform | 构建 | ✅ 完全兼容 | |
moduleParsed | 构建 | ✅ 完全兼容 | |
buildEnd | 构建 | ✅ 完全兼容 | |
renderStart | 输出 | ✅ 完全兼容 | |
banner/footer | 输出 | ✅ 完全兼容 | |
intro/outro | 输出 | ✅ 完全兼容 | |
renderChunk | 输出 | ⚠️ 部分兼容 | 性能考虑 |
generateBundle | 输出 | ✅ 完全兼容 | |
writeBundle | 输出 | ✅ 完全兼容 | |
onLog | 全程 | ✅ 完全兼容 | |
watchChange | watch | ✅ 完全兼容 |
7.2 写一个 Rolldown 专属插件
虽然 Rollup 插件都能用,但 Rolldown 还提供了一些专属能力。比如利用 Rust 侧的 AST 信息做更精确的转换:
// rolldown-plugin-react-optimizer.js
// 利用 Rolldown 的 meta 信息做 React 组件优化
export default function reactOptimizer() {
return {
name: 'react-optimizer',
// transform 钩子 —— 可以拿到 Rolldown 提供的额外信息
transform(code, id, meta) {
// meta.astBody —— Rolldown 提供的 AST 信息(来自 oxc)
// 这比 Rollup 的 this.parse() 快得多
if (!id.endsWith('.tsx') && !id.endsWith('.jsx')) return null;
// 检测是否有 React.memo 可以应用
const hasMemoCandidate = meta.exports.some(
exp => exp.type === 'default' && isComponentExport(exp)
);
if (hasMemoCandidate) {
// 自动包裹 React.memo
return {
code: wrapWithMemo(code),
map: null, // Rolldown 会自动处理 sourcemap
};
}
},
};
}
7.3 插件的性能优化建议
在 Rollup 中,插件是串行执行的——每个模块的 transform 逐个插件处理。这意味着 N 个插件 × M 个模块 = N × M 次处理。
在 Rolldown 中,插件执行模型更高效:
// Rolldown 的插件调度策略
fn run_plugins(module: &Module, plugins: &[Plugin]) -> TransformResult {
// 1. 并行调度:不互相依赖的插件可以并行
let independent_plugins = partition_independent(plugins);
// 2. 流式处理:前一个插件的输出直接传给下一个
// 不需要等所有插件完成
let result = module.source;
for plugin in plugins {
result = plugin.transform(result);
}
result
}
给插件开发者的建议:
- 减少不必要的 transform:尽早
return null跳过不相关的文件 - 避免重复 AST 解析:利用 Rolldown 的
meta信息而不是this.parse() - 缓存转换结果:用
this.cache缓存耗时操作的结果 - 合并插件:多个小插件合并成一个,减少钩子调用次数
// 好的插件写法
export default function myPlugin() {
const cache = new Map();
return {
name: 'my-plugin',
transform(code, id) {
// 1. 快速过滤
if (!id.match(/\.tsx?$/) || id.includes('node_modules')) return null;
// 2. 缓存检查
if (cache.has(id) && cache.get(id).code === code) {
return cache.get(id).result;
}
// 3. 最小化转换
const result = minimalTransform(code);
cache.set(id, { code, result });
return result;
},
};
}
八、生产环境部署 Rolldown
8.1 CI/CD 集成
Rolldown 的 Rust 核心通过 napi-rs 预编译分发,这意味着 CI 环境不需要额外安装 Rust:
# GitHub Actions —— 使用 Rolldown 构建
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build with Rolldown
run: npm run build # rolldown 自动安装预编译二进制
- name: Deploy
run: npm run deploy
对比 Rollup 的构建时间:
# 之前:Rollup 构建
# 平均构建时间:2-3 分钟(大项目)
# CI 成本:每月 $200-500(构建时间长 = 更多 CI 分钟)
# 之后:Rolldown 构建
# 平均构建时间:5-10 秒(大项目)
# CI 成本:每月 $20-50(构建时间短 = 更少 CI 分钟)
8.2 Docker 环境适配
Rolldown 的预编译二进制是平台特定的:
# Dockerfile —— 包含 Rolldown
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci # rolldown 的 @rolldown/binding-linux-x64-gnu 会自动安装
COPY . .
RUN npm run build # Rolldown 构建
# 多架构构建
# docker buildx build --platform linux/amd64,linux/arm64 .
# arm64 需要不同的 binding 包
注意事项:
- Linux x64 GNU 环境:
@rolldown/binding-linux-x64-gnu - Linux x64 MUSL(Alpine):需要检查兼容性
- Windows x64:
@rolldown/binding-win32-x64-msvc - macOS ARM:
@rolldown/binding-darwin-arm64
8.3 构建缓存策略
Rolldown 支持持久化缓存,可以加速增量构建:
export default defineConfig({
// Rolldown 的缓存配置
cache: {
enabled: true,
dir: './.rolldown-cache', // 缓存目录
},
});
缓存的工作原理:
// Rolldown 的缓存策略
struct BuildCache {
// 1. 模块解析缓存
module_parse_cache: HashMap<ModuleId, ParseResult>,
// 2. 插件 transform 缓存
plugin_cache: HashMap<(PluginName, ModuleId), TransformResult>,
// 3. ModuleGraph 子图缓存
graph_cache: HashMap<ModuleIdSet, SubGraph>,
}
// 增量构建流程
fn incremental_build(prev_cache: BuildCache, changed_files: &[PathBuf]) -> BuildResult {
// 1. 识别受影响的模块范围
let affected = compute_affected_range(changed_files, &prev_cache.graph_cache);
// 2. 只重新解析和转换受影响的模块
let new_results = parallel_parse(affected, ctx);
// 3. 合入之前的 ModuleGraph
let updated_graph = merge_graph(prev_cache, new_results);
// 4. 重新 tree-shake 和代码生成
bundle(updated_graph)
}
九、Rolldown vs 其他打包器的全维度对比
9.1 速度对比(19k 模块基准测试)
| 打包器 | 语言 | 首次构建 | 增量构建 | 带sourcemap |
|---|---|---|---|---|
| Rollup | JS | 40.1s | 35.8s | +15s |
| esbuild | Go | 1.70s | 0.9s | +0.3s |
| Rspack | Rust | 4.07s | 1.5s | +0.5s |
| Rolldown | Rust | 1.61s | 0.7s | +0.4s |
| Webpack | JS | 120s+ | 100s+ | +20s+ |
数据来源:rolldown/benchmarks(Ubuntu, 19k React JSX + 9k iconify)
9.2 功能对比
| 功能 | Rollup | esbuild | Rspack | Rolldown |
|---|---|---|---|---|
| Tree-shaking 精度 | 最高 | 保守 | 中等 | 最高 |
| 代码分割 | 精细 | 粗糙 | 精细 | 精细+优化 |
| 插件生态 | 最丰富 | 最少 | webpack兼容 | Rollup兼容 |
| TypeScript | 需插件 | 内置 | 需配置 | 内置 |
| CJS 支持 | 需插件 | 内置 | 内置 | 内置 |
| Module Fed | ❌ | ❌ | ✅ | ✅(1.0+) |
| minify | 需terser | 内置 | 内置 | 内置 |
| sourcemap | 支持 | 支持 | 支持 | 支持(oxc) |
| HMR | 无 | 无 | 支持 | Vite集成 |
9.3 适用场景分析
什么时候选 Rolldown:
- 你用 Vite,想统一开发和生产的打包引擎 → 必选
- 你用 Rollup,但构建速度是瓶颈 → 强烈推荐
- 你做组件库,需要极致 Tree-shaking + 快速构建 → 最佳选择
- 你做微前端,需要 Module Federation + ESM → 新选项
什么时候选 Rspack:
- 你有重度 Webpack 依赖(大量 webpack 插件和 loader)→ 渐进迁移
- 你的团队熟悉 webpack 配置但不想学新 API → 低迁移成本
什么时候选 esbuild:
- 你只在乎速度,不在乎产物体积 → 最快选项
- 你做 CLI 工具或脚本打包(不需要精细 tree-shaking)→ 够用
什么时候继续用 Rollup:
- 你用了 Rolldown 不兼容的插件 → 等兼容性完善
- 你的项目很小(<500 模块),速度差异不明显 → 稳守
十、Rolldown 的未来路线图
10.1 Rolldown 1.x 的演进方向
Rolldown 1.0 是稳定版,但还在快速演进(已经发布了 1.1.0)。接下来的方向:
1. 完整的 Rollup 插件兼容(目标:100%)
当前兼容率约 85-90%,主要缺失的是少数高级钩子(this.load、moduleInfo.hasModuleExport)。这些会在 1.2-1.3 中逐步补全。
2. WASM 绑定
napi-rs 绑定只支持 Node.js 环境。WASM 绑定将让 Rolldown 可以在浏览器中运行,用于在线构建场景(StackBlitz、WebContainers 等)。
3. 增量 HMR
Rolldown 的增量构建已经很快,但 HMR(模块热替换)需要更精细的模块级别更新。这是 Vite 开发体验的核心。
4. 原生 CSS 处理
当前 CSS 处理依赖 Vite 的 CSS 插件。Rolldown 未来可能内置 CSS 解析和打包,进一步减少依赖。
10.2 Vite 9 的展望
Vite 8 用 Rolldown 统一了打包引擎,但还有更多可能性:
Vite 8(当前):
打包引擎 → Rolldown ✅
开发服务器 → Node.js
HMR → Node.js + Rolldown
Vite 9(未来):
打包引擎 → Rolldown ✅
开发服务器 → Rust 原生(Vite Rust)
HMR → Rust 原生
文件监听 → Rust 原生(notify crate)
预期效果:
开发冷启动 → <50ms(大项目)
HMR → <10ms
内存占用 → 减少 50%
这意味着整个前端开发工具链正在从 JavaScript 迁移到 Rust。Rolldown 是这个迁移的关键一步。
10.3 生态影响
Rolldown 的成功不只是 Vite 用户的事。它对整个 JavaScript 生态有深远影响:
1. 打包器标准化
Rolldown 的 Rollup-compatible API 正在成为打包器的标准接口。当一个 API 同时被 Rollup(最精)、Rolldown(最快+最精)、Vite(最流行)采用时,它就是事实标准。
2. Rust 工具链的成熟
Rolldown + oxc + napi-rs 的组合证明了 Rust 可以无缝融入 JavaScript 生态。这条路径会被更多工具效仿——AST 工具、linter、formatter、测试框架都可能走这条路。
3. 插件生态的统一
当 Vite 不再需要双引擎兼容层,插件开发变得简单。这会加速 Vite 插件生态的繁荣。
总结:Rolldown 1.0 的核心意义
Rolldown 1.0 不是一个简单的「更快版 Rollup」。它是一个架构级的选择:
- 速度与精度兼得:用 Rust 的并行和零开销抽象,实现 esbuild 级速度 + Rollup 精度
- 开发与生产统一:消除 Vite 的双引擎割裂,一份配置、一种行为
- 生态兼容而非颠覆:Rollup API + Rollup 插件 = 渐进迁移而非革命
- 基础设施层的 Rust 化:oxc + napi-rs 的路径为 JS 工具链的 Rust 化树立了标杆
对于开发者来说,最直接的感受是:
之前:npm run build → 40 秒 → 盯着进度条 → 去倒杯咖啡
之后:npm run build → 2 秒 → 已经完成了 → 你还没来得及站起来
这不是夸张。这是 Rust 在前端基础设施中的第一次真正的、可用的、生态兼容的胜利。而 Rolldown 1.0,就是这场胜利的里程碑。
参考资源:
- Rolldown 官网:https://rolldown.rs
- GitHub:https://github.com/rolldown/rolldown
- Vite 8 发布说明:https://vitejs.dev/blog/announcing-vite8
- oxc 项目:https://github.com/oxc-project/oxc
- VoidZero(Rolldown 背后公司):https://voidzero.dev
- 性能基准测试:https://github.com/rolldown/benchmarks