编程 Rolldown 深度拆解:当 Rust 把 JavaScript 打包器从 Rollup 手里抢过来——从 OXC 解析、并行图构建到原生 ESM 输出的全链路实战

2026-08-19 00:16:07 +0800 CST views 7

Rolldown 深度拆解:当 Rust 把 JavaScript 打包器从 Rollup 手里抢过来——从 OXC 解析、并行图构建到原生 ESM 输出的全链路实战

2026 年,前端构建工具迎来一个标志性拐点:Vite 8 正式把底层打包器从 Rollup 切换为 Rolldown——一个用 Rust 写成、基于 OXC 解析器、API 完全兼容 Rollup 的新引擎。这不是一次"又一次打包器性能优化",而是前端工程化十余年来"双引擎割裂"问题的终局方案。本文从前端打包器的演进史讲起,拆解 Rolldown 的核心概念、架构设计与性能哲学,并配可运行的代码实战与性能优化清单,带你真正看懂这个正在重写前端基础设施的项目。


一、背景:前端打包器的"三代长征"

要理解 Rolldown 为什么重要,得先理解我们是怎么一步步走到今天的。打包器的历史,本质上是一部"如何让浏览器跑起模块化代码"的妥协史。

1.1 刀耕火种:Browserify 与 webpack 的 CommonJS 时代

2012 年前后,Node.js 把 CommonJS(require / module.exports)带到了服务端,但浏览器原生并不认识这套东西。于是 Browserify 出现了:它把 require 调用静态分析出来,把所有模块拼成一个大文件,在浏览器里模拟一个 require 函数。思路极其朴素,却第一次让"前端也能写模块"成为现实。

真正统治这个时代的是 webpack(2012 年发布,2014 年后爆发)。webpack 的设计哲学是"一切皆模块"——JS、CSS、图片、字体都能被 import,通过 loader 链做转换,通过 plugin 做任意干预。它解决了 Browserify 无力处理的复杂资源依赖,但代价是配置黑洞构建缓慢:一个中型项目 npm run build 跑个几十秒是家常便饭。

1.2 Rollup 的革命:ESM、Tree Shaking 与"库即模块图"

2015 年 ES Module(import / export)标准化,给了社区一个新武器:静态模块结构。Rollup 抓住了这个机会,提出了一个后来被所有打包器效仿的核心理念——

模块不是被"包"进函数里执行的,而是被"摊平"进同一个作用域的。

这就是 Scope Hoisting(作用域提升)。Rollup 通过静态分析 ESM 的导入导出关系,把分散在多个文件里的函数、变量直接合并到一个作用域,只保留真正被用到的部分。这种"基于使用关系的裁剪"就是 Tree Shaking(摇树)

Rollup 因此成了库打包的事实标准:Vue、React、Three.js、几乎所有你想得到的 JS 库,发布到 npm 的 ESM 产物都是 Rollup 打的。它的 API 设计(尤其是插件钩子模型)干净、可组合,深刻影响了后来的每一个打包器。

1.3 速度的诱惑:esbuild、swc、rspack 的 Rust/Go 原生浪潮

Rollup 虽好,但它是用 JavaScript 写的。当项目膨胀到上万个模块时,Node 的单线程事件循环和 GC 成了瓶颈。2020 年,esbuild 横空出世——用 Go 写成,靠并行解析 + 极致优化的字符串处理,把构建速度提升了 10~100 倍。它的 slogan 很直白:"An extremely fast bundler."

随后 swc(Rust 写的 TS/JS 编译器)、rspack(Rust 写的 webpack 兼容打包器)把"原生速度"从噱头变成了标配。大家意识到:前端工具链的下一波红利,不在算法,而在语言

1.4 Vite 的尴尬:dev 用 esbuild、build 用 Rollup 的"双引擎割裂"

2021 年,Vite 用一套巧妙的架构赢得了开发者心智:

  • 开发模式(dev):不打包,直接用浏览器原生 ESM + esbuild 做依赖预构建和 TS/JSX 转译,冷启动秒开,HMR 毫秒级。
  • 生产构建(build):为了产物质量和 Tree Shaking 精度,仍用 Rollup 打包。

这个"因地制宜"的设计非常聪明,但也埋下了双引擎割裂的隐患:

  1. 行为不一致:esbuild 转译和 Rollup 打包对某些语法、某些边界情况的处理不同,导致"dev 好好的,build 就炸了"。
  2. 插件不统一:dev 阶段用 esbuild 的插件体系,build 阶段用 Rollup 的插件体系,同一个需求要写两套逻辑。
  3. 重复劳动:一份源码要流过两套不完全相同的工具链,调试成本翻倍。

Evan You 在多个场合表达过这个痛点的无奈:Vite 本质上是"两个打包器拼在一起"。要真正统一,必须有一个既快(原生)、又兼容 Rollup API(继承生态)、还专为 ESM 设计的打包器。

1.5 Rolldown 登场:VoidZero 的统一答案

2024 年,由 Evan You 创立的 VoidZero 公司(专注 JS 工具链)正式开源 Rolldown。它的定位非常精准:

Rolldown = Rollup 的兼容 API + esbuild 级别的速度 + Vite 的原生集成。

Rolldown 用 Rust 写成,解析与转换层直接复用 VoidZero 自家的 OXC(Oxidation Compiler) 工具链(同一个团队做的 oxlint、oxc-parser 都出自这里),通过 NAPI-RS 暴露给 Node.js。它既能在 vite build 里无缝替换 Rollup,也能独立作为打包器使用。

到 2026 年,Rolldown 已成为 Vite 8 的统一打包器——dev 与 build 终于跑在同一个 Rust 引擎上,双引擎割裂成为历史。这也是本文要深度拆解的主角。


二、核心概念:打包器究竟在做什么

在拆解 Rolldown 之前,先把"打包器到底干了什么"这件事讲透。一个成熟的打包器,内部是一条多阶段流水线

入口(input)
   │
   ▼
① 模块解析(Resolver)   —— 把 "react" 这样的裸说明符解析成具体文件路径
   │
   ▼
② 解析(Parser)         —— 源码 → AST(抽象语法树)
   │
   ▼
③ 转换(Transform)      —— TS/JSX 降级、babel 插件、自定义改写
   │
   ▼
④ 依赖图(Module Graph) —— 顺着 import 把所有模块连成一张有向图
   │
   ▼
⑤ 链接(Link)           —— Tree Shaking + Scope Hoisting,算出最终该保留什么
   │
   ▼
⑥ 代码分割(Chunk Graph)—— 按 entry / 动态 import / manualChunks 切成多个产物
   │
   ▼
⑦ 输出(Output)         —— 生成 ESM / CJS / IIFE 文件 + sourcemap

任何打包器的差异,都体现在这条流水线上某几个环节的取舍。下面挑四个最容易误解的概念讲清楚。

2.1 模块解析:resolver 与 ESM/CJS 的语义鸿沟

import React from 'react' 里的 'react' 不是路径,而是一个裸模块说明符(bare specifier)。Resolver 要按一套规则把它映射到真实文件:

  • node_modules/react/index.js(package.json 的 main / module / exports 字段)
  • 扩展名补全(.ts / .tsx / .js / .jsx / .mjs
  • 路径别名(@/src/,Vite 的 resolve.alias
  • 条件导出("import" vs "require" 字段,区分 ESM/CJS 消费方)

Rolldown 复用了 OXC 的 oxc-resolver,这是一套用 Rust 重写的解析器,比 Node 自带的 require.resolve 和 JS 版 enhanced-resolve(webpack 用的)都快得多,而且行为对 ESM/CJS 双规范的语义处理更严谨。

2.2 Tree Shaking 的本质:为什么只有 ESM 能被摇掉

Tree Shaking 的目标是:只把被用到的代码打进产物,丢掉死代码。它成立的前提是"导入导出在编译期可见且不可变"。

  • ESM 的 import / export 是静态的import { foo } from './a' 在解析阶段就能确定 foo 从哪来、被谁用。于是打包器可以安全地删掉 a.js 里没被引用的 barbaz
  • CommonJS 的 require / module.exports 是动态的require(someVar)module.exports[x] = y 在运行时才能确定,静态分析无从下手。所以 CJS 模块基本无法被有效摇树。

这也是为什么现代库都尽量发布 ESM("type": "module"exports.import)。Rolldown 在链接阶段做 Tree Shaking,依赖两路信号:

  1. 模块语法层面:ESM 的导出是否被某个导入绑定引用。
  2. 副作用标记:package.json 的 "sideEffects": false 或源码里的 /*#__PURE__*/ 注解,告诉打包器"这个调用没有副作用,没用到就能删"。

2.3 Scope Hoisting:把"函数包裹地狱"摊平

早期打包器(包括 webpack 早期、我们的教学 mini-bundler)会给每个模块套一个函数:

// 非 hoisting:每个模块是一个闭包
__modules['a.js'] = function (module, exports, require) { /* ... */ }
__modules['b.js'] = function (module, exports, require) { /* ... */ }

模块一多,闭包层层嵌套,不仅有函数调用开销,还给 V8 的优化(内联、逃逸分析)制造障碍。

Scope Hoisting 的做法是:把所有模块的顶层声明重命名(加前缀避免冲突)后,平铺到同一个作用域,只保留入口的执行顺序:

// hoisting 后:a 和 b 的顶层符号被摊平到同一个作用域
function a_foo() { /* ... */ }
function b_bar() { a_foo() }
b_bar();

这样产物更短、运行更快、V8 更容易优化。Rollup 是 Scope Hoisting 的鼻祖,Rolldown 在 Rust 里重做了同一套算法——而且因为类型系统和并行化的加持,处理超大图时更稳定。

2.4 代码分割与 Chunk Graph

单文件产物在大型应用里不可行(首屏加载会拖死)。打包器通过动态 import()手动分包把模块切成多个 chunk:

  • 每个 entry 是一个 chunk 起点;
  • 每个 import('./x') 动态导入会切出一个异步 chunk;
  • manualChunks 允许你按规则把稳定依赖(如 reactlodash)拆成独立 vendor chunk,利用浏览器长缓存。

Rolldown 的 Chunk Graph 构建同样在 Rust 中完成,能高效处理数万个模块之间的共享依赖,自动把"被多个 chunk 共享的模块"提取成公共 chunk,避免重复打包。

2.5 教学时间:60 行看懂 pipeline(CJS 版)

为了让你对"打包器在做什么"有肌肉记忆,下面写一个教学级 mini 打包器(CommonJS 风格,仅示意核心思想,不处理别名/扩展名/TS):

// mini-bundler.mjs —— 仅用于理解 pipeline,60 行级别
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'

const seen = new Map()
const graph = []

// ① 顺着 import 走,构建模块图(深度优先)
function walk(id) {
  if (seen.has(id)) return
  seen.set(id, true)
  const code = readFileSync(id, 'utf8')
  graph.push({ id, code })
  const imports = [...code.matchAll(/import\s+.*?from\s+['"](.+?)['"]/g)]
  for (const m of imports) walk(resolve(dirname(id), m[1]))
}

// ② 把模块包成函数,用 CJS 风格模拟 require/exports
function bundle(entry) {
  walk(resolve(entry))
  let body = ''
  for (const { id, code } of graph) {
    body += `__m[${JSON.stringify(id)}] = function (module, exports, require) {\n${rewrite(code)}\n}\n`
  }
  return `(function () {
    const __m = {}, __c = {};
    function require(id) {
      if (__c[id]) return __c[id].exports;
      const m = { exports: {} };
      __c[id] = m;
      __m[id](m, m.exports, require);
      return m.exports;
    }
    ${body}
    require(${JSON.stringify(resolve(entry))});
  })()`
}

// ③ 极简改写:import/export → require/exports
function rewrite(code) {
  return code
    .replace(/import\s+(\w+)\s+from\s+['"](.+?)['"]/g, 'const $1 = require("$2");')
    .replace(/export\s+default\s+/g, 'module.exports = ')
    .replace(/export\s+const\s+(\w+)\s*=/g, 'module.exports.$1 = ')
}

writeFileSync('dist/mini-bundle.js', bundle(process.argv[2]))

node mini-bundler.mjs src/main.js 就能得到一个可执行的 bundle。但请注意:这只是"教学模型"——它用的是 CJS 包裹(没有 Scope Hoisting),用正则改写(没有真正的 AST,遇到复杂语法会崩),单线程(没有并行),也不做 Tree Shaking。

Rolldown 做的正是同一件事,但

  • 解析用 OXC(真正的 Rust AST,支持 TS/JSX/所有边缘语法);
  • 链接用 Scope Hoisting + Tree Shaking(基于 ESM 语义,不是正则);
  • 构建在 Rust 并行调度下完成(数万模块不卡);
  • 输出 原生 ESM(可被浏览器和 Node 直接消费,无需运行时代理)。

理解了 mini-bundler,再去看 Rolldown,你会发现它只是把这条流水线在"工程正确性 + 原生速度"两个维度上做到了极致。


三、架构分析:Rolldown 为什么快

Rolldown 的快,不是玄学,是架构选择的必然结果。它大致分为三层:

┌─────────────────────────────────────────┐
│  JS Facade (rolldown / rolldown-vite)    │  ← 开发者调用的 API、配置、插件
├─────────────────────────────────────────┤
│  NAPI Binding (@rolldown/binding)        │  ← Rust ↔ Node 的零成本桥接
├─────────────────────────────────────────┤
│  Rust Core (crates/)                     │  ← 解析/转换/图构建/链接/输出
│    ├─ oxc-parser / oxc-transform         │
│    ├─ module graph + rayon 并行         │
│    └─ chunk graph + codegen + minify    │
└─────────────────────────────────────────┘

3.1 解析层用 OXC:比 swc 快 3x 的解析器

打包器的第一道瓶颈是解析——把源码变成 AST。Rolldown 直接站在 VoidZero 自家的 OXC(Oxidation Compiler) 肩膀上:

  • oxc-parser:官方基准显示它比 swc(Rust 写的标杆)快约 3 倍,比 Biome 快约 5 倍;
  • oxc-transform:承担 TS / JSX / 语法降级等转换,替代了原先 Vite 里 esbuild 的转译职责;
  • oxc-resolver:高速模块解析。

一个容易被忽视的点:当解析和转换都在 Rust 里完成时,dev 和 build 就共享了同一套语义。这正是消灭"双引擎割裂"的关键——你写的 TS、你用的 JSX,在 dev 和 build 里都走同一条 OXC 路径,行为天然一致。

3.2 模块图构建在 Rust 中完成

模块图(谁 import 了谁)是打包器的核心数据结构。Rolldown 在 Rust 侧用强类型维护这张图,避免把成吨的 JS 对象在 Node 堆里来回搬运。每个模块的 AST、作用域信息、导入导出绑定都在 Rust 内存里就地分析。

3.3 并行:Rayon 数据并行

数万个模块之间没有依赖的部分,天然可以并行处理。Rolldown 用 Rust 的 Rayon 数据并行库,把"解析 + 转换 + 图构建"这类可并行的任务摊到多核上。相比 webpack 用 JS 写、受限于 Node 单线程(即便用 worker_threads 也要跨线程序列化开销),Rolldown 的并行是"零序列化成本"的原生多线程。

3.4 与 Rollup 兼容的插件模型

这是 Rolldown 最聪明的设计决策之一:它不发明新插件 API,而是兼容 Rollup 的钩子模型

// 同一套钩子,在 Rollup 和 Rolldown 都能跑
{
  name: 'my-plugin',
  resolveId(source, importer) { /* ... */ },
  load(id) { /* ... */ },
  transform(code, id) { /* ... */ },
  renderChunk(code, chunk) { /* ... */ },
  generateBundle(options, bundle) { /* ... */ },
}

这意味着 Vite 生态里成千上万的 Rollup 插件,几乎可以零改动迁移到 Rolldown。生态不是从零开始的,而是直接继承了 Rollup 十年积累的护城河。对 Vite 用户来说,从 vite 切到 rolldown-vite,绝大多数项目改个 import 就能跑

3.5 内置 transform & minify:干掉双引擎

传统 Vite 里,dev 的转译靠 esbuild,build 的压缩靠 esbuild/terser。Rolldown 把转换和压缩都收编进自己的 Rust 核心(基于 OXC 的 transform 与 minify),于是:

  • dev 不再需要 esbuild 做 TS/JSX 降级(OXC 直接干);
  • build 的 minify 也不再强依赖 esbuild(虽然仍可切换)。

一条流水线,覆盖 dev 和 build 的全部转换需求。这才是"统一引擎"的真正含义。

3.6 Rolldown vs rspack vs esbuild:三条不同的哲学

很多人把这几个工具混为一谈,其实它们哲学完全不同:

维度Rolldownrspackesbuild
语言RustRustGo
兼容对象Rollup APIwebpack 插件/配置自有 API
设计目标Vite 的统一引擎webpack 的 Rust 提速极致速度的通用打包
Tree Shaking强(ESM 语义)强(沿用 webpack)弱(主要靠标记)
适用场景库 + 应用(Vite 系)存量 webpack 项目迁移快糙猛的 dev/小构建
插件生态来源Rollup 生态webpack 生态自有(较少)

一句话总结:

  • rspack 回答的是"怎么让 webpack 项目不重写就变快";
  • esbuild 回答的是"怎么用最少的功能做到最快";
  • Rolldown 回答的是"怎么让 Vite 的 dev 和 build 用同一个快引擎"。

它们不互斥——很多团队 dev 用 esbuild、应用构建用 rspack、库构建用 Rolldown,各取所需。但站在 Vite 视角,Rolldown 是"官方钦定"的终局。

### 3.8 Rolldown 的内部数据流:从 input 到 chunk 的一次完整旅行

把前面所有概念串成一条真实的执行路径,能帮你建立"它在内存里到底发生了什么"的画面感:

  1. 入口登记:你把 input: 'src/main.js' 交给 Rolldown。引擎先在 Rust 侧建一个空的模块图。
  2. 解析 + 解析器递归:从入口开始,oxc-resolver 把裸说明符解析成绝对路径,oxc-parser 把它变成 AST。遇到 import,就顺着路径继续解析下一个模块——这一步在 Rayon 并行下,可以一次性展开成百上千个模块的 AST,而不是 Node 里的逐个 await
  3. 转换:每个模块的 AST 经过 oxc-transform 做 TS/JSX 降级、define 常量替换、自定义插件 transform 钩子改写。因为都在 Rust 里,没有"把 AST 序列化回 JS 对象再传回 Node"的往返开销。
  4. 建图 + 作用域分析:所有模块的导入导出绑定被连成有向图,Rolldown 做"谁引用了谁、哪些导出是死的"的全局分析。这是 Tree Shaking 和 Scope Hoisting 的数据基础。
  5. 链接(Link):基于上一步的分析,把活着的符号摊平到统一作用域,生成"被保留的、重命名后的"中间表示(IR)。死代码在这一步被彻底丢弃。
  6. 切块(Chunking):根据 entry、动态 import()manualChunks,把 IR 切成多个 chunk,并提取共享模块为公共 chunk。
  7. 代码生成 + 压缩 + sourcemap:每个 chunk 生成最终文本,套用 minify,产出 .map。全部通过 NAPI 一次性回传 Node,写入磁盘。

整个过程中,JS 层只负责"描述意图"(配置、插件逻辑、钩子回调),重活全部在 Rust 侧完成。这就是为什么同样的逻辑,Rolldown 比纯 JS 打包器快出一个量级——不是某一步快,而是"每一层都不在 Node 里空转"。

3.9 为什么 Rust 是写打包器的"正解"

有人会问:Go(esbuild)也能原生速度,为什么 Rolldown 选 Rust?除了 VoidZero 团队的技术偏好,还有一个工程理由:Rust 的零成本抽象 + 强类型,让"数万模块图"这种复杂数据结构既快又不易出错。打包器内部要维护带环依赖、带副作用标记的图,用 Rust 的枚举和所有权模型表达,比在 GC 语言里手动管理引用关系更稳。再加上 Rayon 的并行几乎"免费",以及 NAPI-RS 让 Rust↔Node 调用如同本地函数——这套组合拳,恰好是打包器这种"计算密集 + 图结构"场景的最优解之一。

3.7 基准数据解读

Rolldown 官方公布的基准(打包约 19k 模块:1 万 React JSX 组件 + 9k iconify 文件,含压缩与 sourcemap):

工具耗时
Rolldown1.61s
esbuild1.70s
rspack4.07s
Rollup + esbuild40.10s

几个关键解读:

  1. Rolldown 在带压缩、带 sourcemap、做完整 Tree Shaking 的前提下,追平甚至略快于 esbuild——而 esbuild 本身几乎不做精细摇树。也就是说 Rolldown 是"又快又好"。
  2. 对比 Rollup + esbuild(老 Vite build 的架构):快了约 25 倍。这就是统一引擎对"双引擎拼接"的碾压。
  3. 数字会随版本浮动,但量级关系稳定:原生 Rust + 并行 + 统一流水线,对 JS 写的 Rollup 是降维打击。

四、代码实战

讲了这么多原理,动手才是最有说服力的。下面所有示例都能在 Node 18+ 环境跑起来。

4.1 最小可用配置

独立使用 Rolldown 时,配置几乎和 Rollup 一模一样:

// rolldown.config.js
import { defineConfig } from 'rolldown'

export default defineConfig({
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'esm',      // 输出原生 ESM
    sourcemap: true,    // 生成 sourcemap
    entryFileNames: '[name].js',
    chunkFileNames: 'chunks/[name]-[hash].js',
  },
  // 解析与转换相关选项(OXC 驱动)
  resolve: {
    alias: { '@': new URL('./src', import.meta.url).pathname },
  },
  define: {
    __VERSION__: JSON.stringify('1.0.0'),
  },
})

跑构建:

npx rolldown build --config rolldown.config.js

4.2 通过 Node API 调用

除了配置文件,你也可以在代码里直接调用 Rolldown(适合写自定义构建脚本、CI 工具):

// build.mjs
import { rolldown } from 'rolldown'

const bundle = await rolldown({
  input: 'src/main.js',
  plugins: [
    {
      name: 'log-modules',
      buildEnd() {
        console.log('build finished, modules:', this.meta?.moduleCount ?? 'n/a')
      },
    },
  ],
})

await bundle.write({
  dir: 'dist',
  format: 'esm',
  sourcemap: true,
})

// 别忘了 close,释放底层 Rust 资源
await bundle.close()

注意 bundle.close():因为底层是 Rust 持有资源,不主动关闭可能在长进程(如 watch 模式、CI 循环)里泄漏。这是和纯 JS 打包器体验上略有不同的地方。

4.3 写一个自定义插件(两个实例)

Rolldown 的插件 API 与 Rollup 兼容,下面写两个实用且可运行的插件。

插件一:注入构建信息

// plugins/inject-build-info.mjs
export function injectBuildInfo() {
  return {
    name: 'inject-build-info',
    // 在转换阶段替换占位符
    transform(code, id) {
      if (!id.endsWith('.js')) return null
      if (!code.includes('__BUILD_TIME__')) return null
      const replaced = code.replace(
        /__BUILD_TIME__/g,
        JSON.stringify(new Date().toISOString())
      )
      return { code: replaced, map: null }
    },
  }
}

源码里写 const BUILD_TIME = __BUILD_TIME__,打包后会被替换成真实时间戳。define 更适合全局常量,transform 更适合"按文件内容动态改写"。

插件二:模块依赖图统计(体现 Rolldown 钩子的真实能力)

// plugins/graph-stats.mjs
export function graphStats() {
  const edges = []
  return {
    name: 'graph-stats',
    // resolveId 拿到"从哪 import 了什么"
    resolveId(source, importer) {
      if (importer) edges.push(`${importer} → ${source}`)
      return null // 不拦截,交给默认 resolver
    },
    buildEnd() {
      const uniq = [...new Set(edges)]
      console.log(`\n[graph-stats] 共 ${uniq.length} 条依赖边`)
      // 找出被引用最多的模块(粗略的"热点依赖")
      const count = {}
      for (const e of uniq) {
        const to = e.split(' → ')[1]
        count[to] = (count[to] || 0) + 1
      }
      const top = Object.entries(count)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
      console.log('[graph-stats] 被引用最多的模块:')
      for (const [m, c] of top) console.log(`  ${c}  ${m}`)
    },
  }
}

把这两个插件塞进 rolldown.config.jsplugins 数组即可。这种"在解析阶段顺手采集依赖拓扑"的能力,正是做构建可视化、循环依赖检测、体积归因的基础。

4.4 Rolldown-Vite 迁移

这是大多数前端工程师最关心的场景。Rolldown 作为 Vite 8 的默认引擎,迁移成本极低:

# 安装 rolldown 版 Vite(与 vite 完全兼容 API)
npm i -D rolldown-vite

然后把 vite.config.ts 里的 import 换掉:

// 原来
// import { defineConfig } from 'vite'
// 改成
import { defineConfig } from 'rolldown-vite'

export default defineConfig({
  // 你的配置几乎原样保留
  plugins: [/* 原 Rollup 插件基本都能用 */],
  build: {
    // rolldown 选项可以直接挂在这里
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
})

几个迁移注意点(实战经验):

  1. 插件兼容性:纯 Rollup 插件基本无缝;但若某个插件重度依赖 this.parsethis.emitFile 等少数钩子的非标准行为,可能要在 Rolldown 下微调。先用 --mode 跑一遍构建对比产物体积。
  2. CSS 处理:Rolldown 对 CSS 的处理路径和老 Vite 略有差异,涉及 CSS Modules / 预处理器时建议核对产物。
  3. dev 与 build 一致性:迁移后最大的体感变化是——dev 看到的报错和 build 完全一致,因为都走 OXC。以前"dev 正常 build 炸"的玄学问题会大幅减少。
  4. define 语义:Rolldown 的 define 在 Rust 侧做常量替换,比 esbuild 的字符串替换更严谨,注意别写出 define: { x: someObject } 这种会被 JSON.stringify 的坑。

4.5 产物与 sourcemap 验证

构建完成后,验证产物才是工程师的素养:

# 看产物体积分布
du -sh dist/*

# 看是否有 sourcemap(排查线上问题时救命)
ls dist/*.map

# 用 Node 直接跑 ESM 产物(确认可独立执行)
node dist/main.js

如果产物体积异常大,下一步就进入性能优化环节。


五、性能优化实战

Rolldown 已经很快,但"快"和"在你的项目上最快"之间,还差一份针对性的优化清单。

5.1 manualChunks:分包是首屏优化第一刀

把高频稳定依赖拆成独立 vendor chunk,让浏览器长缓存命中:

// rolldown.config.js
export default defineConfig({
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'esm',
  },
  // 函数式 manualChunks:按模块路径精细控制
  manualChunks(id) {
    if (id.includes('node_modules')) {
      if (id.includes('react')) return 'react-vendor'
      if (id.includes('lodash')) return 'lodash-vendor'
      return 'vendor' // 其余第三方统一进 vendor
    }
  },
})

经验法则:

  • 框架级依赖(react / vue)单独成块,版本稳定,缓存命中率高;
  • 大但少改的库(如某个图表库)单独成块;
  • 业务代码保持细粒度,方便按路由动态加载。

5.2 target / define / 浏览器目标

Rolldown 的 OXC 转换可以按 target 做语法降级,避免打进用不到的 polyfill:

export default defineConfig({
  input: 'src/main.js',
  output: { dir: 'dist', format: 'esm' },
  // 只为目标浏览器降级,少打 polyfill
  target: 'chrome120',
  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
  },
})

降级范围越窄,产物越小、运行越快。现代项目如果只支持 evergreen 浏览器,甚至可以 target: 'es2022',让原生 async / ?. 语法原样保留。

5.3 minify 选项与取舍

Rolldown 内置基于 OXC 的 minify。你可以对比不同压缩策略的产物体积:

export default defineConfig({
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'esm',
    // minify 可细化控制
    minify: {
      // 压缩 JS
      compress: true,
      // 缩短局部变量名
      mangle: true,
      // 压缩产物里的 whitespace(通常开启)
      whitespace: true,
    },
  },
})

取舍建议:

  • 应用产物compress + mangle + whitespace 全开,体积优先;
  • 需要被别人再打包的库:谨慎开 mangle(会改导出名),通常用 compress + whitespace,保留可读符号;
  • 调试阶段:临时关掉 minify,让 sourcemap 和新跑出来的代码一一对应,定位问题更快。

5.4 lazy compilation(实验特性)

超大项目里,即使 Rolldown 很快,一次性编译所有模块在 dev 启动时仍有开销。Rolldown / rolldown-vite 提供惰性编译(lazy compilation):只编译当前真正被请求到的模块图,按需展开。

// rolldown-vite 下
export default defineConfig({
  build: {
    rollupOptions: {
      // 实验:惰性编译入口以外的模块
      // 具体开关随版本演进,请以官方文档为准
    },
  },
})

注意:惰性编译主要优化 dev 体验,对 production 构建的最终产物体积无影响。它适合" monorepo 里只改了一个子包"的场景。

5.5 常见瓶颈与排查清单

实战中 Rolldown 慢,往往不是 Rolldown 本身,而是项目结构的锅:

  1. 巨型单依赖:某个 node_modules 包导出了上万小文件(如老版 icon 库),解析成本爆炸。→ 用 manualChunks 隔离,或换按需导入的包。
  2. 循环依赖:A→B→A 会让图分析变复杂,且可能破坏 Tree Shaking 假设。→ 用 4.3 的 graph-stats 插件揪出循环边。
  3. CJS 第三方包:无法被摇树的 CJS 依赖会显著拖大产物。→ 优先选 ESM 版本,或用插件把 CJS 转 ESM(注意副作用)。
  4. 重复打包:同一个库被多个入口各自打包一份。→ 检查 node_modules 里是否有多个版本,统一版本号。
  5. sourcemap 过胖:超大项目开 sourcemap 会拖慢写盘。→ CI 里可以只给 production 产物开,dev 用 cheap 模式。

六、总结与展望

把前面所有内容收拢成一句话:

Rolldown 的真正意义,不是"又一个更快的 Rollup",而是"dev 与 build 的统一引擎"——它用 Rust + OXC 抹平了 Vite 长期存在的双引擎割裂,用 Rollup 兼容 API 继承了十年生态,用原生并行把构建从"等一杯咖啡"变成"眨一次眼"。

几个值得记牢的判断:

  1. 对 Vite 用户:Rolldown 是默认未来。迁移成本极低(改个 import),收益是 dev/build 行为一致 + 构建提速一个数量级。已经在 Vite 8 上,无需犹豫。
  2. 对库作者:Rollup 的"库打包标准"地位短期不会变,但 Rolldown 因为同源 OXC、更快、且 API 兼容,正在成为新的备选。新项目可以双跑对比产物体积。
  3. 对 webpack 存量项目:别硬上 Rolldown,先评估 rspack——它才是 webpack 生态的原生提速答案。Rolldown 的哲学是"Rollup 兼容",不是"webpack 兼容"。
  4. 对工具链工程师:OXC 生态(oxlint、oxc-parser、oxc-resolver、oxc-transform)才是 Rolldown 背后真正的"基础设施红利"。Rust 重写前端工具链的大趋势不可逆。

决策指南:你的项目该不该现在上 Rolldown

不是所有项目都该盲目追新。给你一张速查表:

你的现状建议
Vite 7/8 应用,纯 Rollup 插件直接切 rolldown-vite,几乎零成本
大量自定义 Rollup 插件、强依赖冷门钩子先双跑对比产物,再决定
webpack 存量大项目rspack,不是 Rolldown
要发布 npm 的库Rollup 仍稳妥,Rolldown 可作备选双跑
对构建稳定性极度敏感(金融/医疗)等一个 LTS 版本再上

迁移 checklist(落地用):

  1. npm i -D rolldown-vite,改 vite.config 的 import;
  2. npm run build 跑通,对比产物体积(应持平或更小);
  3. 用 4.3 的 graph-stats 插件 + 老产物做一次"模块差异 diff";
  4. 跑一遍 npm run dev,确认 dev/build 报错一致;
  5. 检查 sourcemap 能否正确映射到源码(线上排障刚需);
  6. CI 里加一道"Rolldown 构建产物体积阈值告警",防止回归。

局限与风险(保持诚实)

  • 生态成熟度:Rolldown 相对 Rollup 仍年轻,个别冷门插件钩子的边界行为可能不同,生产前务必做产物 diff。
  • 调试心智:底层是 Rust,出错时的堆栈和 Node 插件作者熟悉的 JS 堆栈不同,需要适应。
  • 版本演进快:Vite 8 / Rolldown 的 API 与配置项仍在快速收敛,本文示例以"兼容 Rollup 的核心钩子"为稳定锚点,具体开关请对照你所用版本的官方文档。

给前端工程师的最后建议

如果你今天只做一件事:把团队项目从 vite 切到 rolldown-vite,跑一遍 build,对比产物体积和构建时间。大概率你会得到一个"又快又一致"的结果——然后你就会理解,为什么 2026 年会被记作"前端构建工具统一元年"。

打包器的长征走了十四年,从 Browserify 的 require 模拟,到 Rollup 的 Scope Hoisting,到 esbuild 的原生速度,再到今天的 Rolldown 统一引擎。工具的终点不是更快,而是让你忘记工具的存在——你只管写模块,剩下的,交给 Rust。


本文基于 Rolldown 官方基准(打包约 19k 模块:Rolldown 1.61s / esbuild 1.70s / rspack 4.07s / Rollup+esbuild 40.10s)与 OXC 工具链公开资料撰写。配置示例以 Rollup 兼容 API 为稳定锚点,具体开关请以你所使用版本的官方文档为准。

推荐文章

Vue3中如何处理路由和导航?
2024-11-18 16:56:14 +0800 CST
gin整合go-assets进行打包模版文件
2024-11-18 09:48:51 +0800 CST
微信小程序开发资源汇总
2026-05-11 16:11:29 +0800 CST
前端如何优化资源加载
2024-11-18 13:35:45 +0800 CST
程序员茄子在线接单