背景
采用 Rust 编写的高性能打包器 rolldown-vite,在实际打包中速度通常能比 Vite(基于 Rollup)快一倍左右。Rolldown 之所以比 Rollup 更快,主要有以下几个原因:
- 底层语言的差异
- Rollup 是用 JavaScript 实现的
- Rolldown 则是用 Rust 实现的 Rollup 兼容打包器。Rust 在执行效率和内存管理方面远胜 JavaScript,因此在相同逻辑下,Rust 版本的打包速度自然更快。
- 更强的并行能力
- Rollup 的构建流程基本是单线程的(受限于 Node.js)
- Rolldown 基于 Rust,可以利用多线程并行解析和处理依赖,充分发挥 CPU 多核的优势。结果就是:Rollup 需要逐个文件处理,而 Rolldown 可以成批处理文件。
- 更高效的 AST 解析
- 打包过程中需要不断将 JS/TS 转换为 AST(抽象语法树)并进行依赖分析
- Rolldown 采用了自研的 Rust 工具链 Oxc(涵盖 parser、transformer、resolver、minifier 等组件),相比传统基于 Babel/Terser 的 JS AST 解析,速度快得多。
- 缓存与增量编译优化
- Rolldown 内部实现了更细粒度的缓存机制,可以避免重复解析同一个依赖
- 在
vite dev
模式下尤为明显:修改一个文件时,Rolldown 只会重新编译受影响的模块,而不是像 Rollup 那样做很多无关的检查。
除此之外,Rolldown 完全兼容 Vite 的配置,迁移成本极低,因此想在项目中应用起来,现在我们进入今天的正题
改造
第一步 修改package.json
添加用rolldown-vite替换vite的声明, 这里有个细节要注意一下,就是rolldown-vite的版本, 如果你写成@latest
,会安装最新的rolldown-vite@7.1.5
版本,这个版本要求node最少为v22.19, 而公司Jenkins CI机器上的node版本是v18, 所以我没有使用最新的版本, 指定了一个兼容node v18的版本
js
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@6.3.16"
}
}
第二步 解决ace-builds引起的报错
执行pnpm i
将vite换成rolldown-vite,运行项目, 不出意外意外果然发生了, 业务代码中引入的ace-builds
js
import { VAceEditor } from 'vue3-ace-editor';
import 'ace-builds/src-noconflict/theme-chrome';
import 'ace-builds/src-noconflict/theme-monokai.js';
import 'ace-builds/src-noconflict/mode-python.js';
import 'ace-builds/src-noconflict/mode-sh.js';
在控制台报如下错误:
js
17:16:48 [vite] (client) Pre-transform error: Failed to resolve import "../lib/dom" from "node_modules/.vite/deps/ace-builds_src-noconflict_theme-chrome.js?v=a245d48f". Does the file exist?
Plugin: vite:import-analysis
File: D:/project/ai-platform-frontend/node_modules/.vite/deps/ace-builds_src-noconflict_theme-chrome.js?v=a245d48f:2:45
1 | import * as __require_for_vite_fUx58Z from "./chrome-css";
2 | import * as __require_for_vite_0FJVYG from "../lib/dom";
| ^
3 | import { __commonJS } from "./chunk-51aI8Tpl.js";
4 |
产生这个错误的原因是: 导入的 ace-builds/src-noconflict/theme-chrome.js
里面其实是个 UMD/CommonJS 打包产物,代码大概是这样的:
js
require("../lib/dom");
在 src-noconflict
目录下确实没有 ../lib/dom
这个文件。 但 Vite(基于 esbuild 的预构建逻辑)在扫描依赖时,会做一层 fallback alias 处理 ,会把 ace 的 require("../lib/dom")
内联成空模块),所以在 Vite 里能跑通。
可是 rolldown-vite
目前的 CommonJS 兼容实现还不完整,它会老老实实去找 ../lib/dom
,于是就报 Does the file exist?
。
解决办法: 官方推荐用 src-min-noconflict
(精简预构建版本),避免加载到内部 require
。将原来的src-noconflict
路径全部替换成src-min-noconflict
,这样就不会出现对 ../lib/dom
的引用 。
js
import { VAceEditor } from 'vue3-ace-editor';
import 'ace-builds/src-min-noconflict/theme-chrome';
import 'ace-builds/src-min-noconflict/theme-monokai';
import 'ace-builds/src-min-noconflict/mode-python';
import 'ace-builds/src-min-noconflict/mode-sh';
第三步 解决vue3-ace-editor引起的报错
一波刚平,一波又起,引入的第三方组件 vue3-ace-editor
, 也报同样的错误:
js
18:36:37 [vite] Internal server error: Failed to resolve import "../lib/dom" from "node_modules/.vite/deps/vue3-ace-editor.js?v=7e2ec517". Does the file
exist?
Plugin: vite:import-analysis
File: D:/project/ai-platform-frontend/node_modules/.vite/deps/vue3-ace-editor.js?v=7e2ec517:10:45
8 | import * as __require_for_vite_cLUzHK from "./default_english_messages";
9 | import * as __require_for_vite_r9MT0S from "./textmate-css";
10 | import * as __require_for_vite_xoz43m from "../lib/dom";
| ^
11 | import * as __require_for_vite_FEdWm0 from "./lib/lang";
12 | import * as __require_for_vite_nDfAqB from "./lib/net";
查看了一下vue3-ace-editor
的源码, 发现里面也有对ace-builds的引入,难怪报出和原来一模一样的错
js
import ace, { type Ace } from 'ace-builds';
可这是第三方包,不是自己写的业务文件。改还是不改呢?看了一下vue3-ace-editor里面的功能不是很复杂,决定对vue3-ace-editor
打补丁, 可以看到有ts和js两个版本。

刚开始选择的是ts版本, 因为项目中用的是ts。 在项目src\components\
创建 vue3-ace-editor
文件夹, 将node_modules/vue3-ace-editor
下的index.ts
,index.d.ts
,types.d.ts
复制粘贴到src\components\vue3-ace-editor
目录下。src\components\vue3-ace-editor\index.ts
文件需要关注的地方如下:
js
import ace, { type Ace } from 'ace-builds';
import ResizeObserver from 'resize-observer-polyfill';
// ...
移除和补充安装npm依赖包
bash
pnpm remove vue3-ace-editor && pnpm add ace-builds resize-observer-polyfill
修改ace的导入方式,将esm导入方式修改成umd加载方式
js
// import ace from 'ace-builds';
import { type Ace } from 'ace-builds';
import ResizeObserver from 'resize-observer-polyfill';
// ...
将node_modules\ace-builds\src-min\ace.js
复制到根目录public\ace.js
,在index.html中添加
js
<script src="./ace.js"></script>
因为这个在线代码编辑器在项目中多处用到,再加上是压缩版本,所以全局加载对网站流量的浪费不是很大。改完之后又发现, 从ace-builds
导出类型定义
js
import { type Ace } from 'ace-builds'
会引入ace/lib/es6-shim
这样的文件, 而ace/lib
文件在node_modules/ace-builds
下是不存在的。
js
node_modules/.pnpm/ace-builds@1.39.0/node_modules/ace-builds/src-noconflict/ace.js?v=f6576fbe:5:45
3 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4 | };
5 | import * as __require_for_vite_ikxleh from "./es6-shim";
| ^
6 | import * as __require_for_vite_0lQ2hq from "./deep_copy";
7 | import * as __require_for_vite_vSNRCb from "./useragent";
本来想把用到的类型拷贝到项目中,可是发现层级依赖很多。改不动。于是将ts版本换成了js版本。删除了index.js
中的import ace from 'ace-builds';
,并将业务代码中vue3-ace-editor
引入路径进行替换, 运行了一下发现vue3-ace-editor
终于不报ace-builds
文件引入找不到的错误了。
js
// 原来
import { VAceEditor } from 'vue3-ace-editor';
// 修改成:
import { VAceEditor } from '@/components/vue3-ace-editor';
第四步 解决recorder-core的wav.js报错
如下的代码, 在本地并不报错, 可是打包部署到线上环境之后
js
import Recorder from 'recorder-core';
import 'recorder-core/src/engine/wav.js'
报错,不能从undefined对象中读取i18n属性
js
index.vue:42
TypeError: Cannot read properties of undefined (reading 'i18n') at wav.js:16:30
wav.js:16
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'i18n') at wav.js:16:30
看了一下recorder-core/src/engine/wav.js
中报错的代码片段
js
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
是win.Recorder对象不存在导致的, 造成这个错误的原因是:recorder-core
的工作模式采用的是全局挂载的设计模式
- 首先,它的核心脚本
src/recorder-core.js
会先执行,在全局的window
对象上创建一个名为Recorder
的主对象。 - 然后,像
wav.js
、mp3.js
这样的引擎(engine)或插件脚本再执行。这些插件脚本会假设window.Recorder
已经存在,并把自己注册到window.Recorder
对象上,或者从它那里获取配置和方法(比如i18n
)。 - Vite (Rollup): 在生产打包时,Rollup 的算法通常能很好地遵循 ES Module 的
import
顺序,确保依赖先被执行。先导入import Recorder from 'recorder-core'
, 然后import 'recorder-core/src/engine/wav.js'
时,它能保证前者创建window.Recorder
的对象在后者的使用时存在。 - rolldown-vite: Rolldown 作为一个新兴的、用 Rust 编写的高性能打包器,其内部的依赖图分析、代码分割(chunking)和模块排序算法与 Rollup 有所不同。在打包优化过程中,因为是并行处理文件, 错误地将它们分割到了不同的 chunk 中并以错误的顺序加载。这就导致
wav.js
运行时,window.Recorder
还未被定义,从而引发了错误。
简单来说,这是一个典型的因打包器优化导致代码执行时序错乱的问题。知道了原因, 解决起来就比较容易了。采用动态加载的方式, 就能保证执行顺序。
js
import Recorder from 'recorder-core';
// @ts-ignore
async () => await import('recorder-core/src/engine/wav.js');
改完之后,果然不报错了。
最后
项目终于不报错了, 让我们看看改造前后的打包时间。两种编译工具的打包时间如下所示:
编译工具 | 时间 |
---|---|
vite6 |
1m 21s |
rolldown-vite@6.3.16 |
39.72s |
效率的提升还是很明显的, 今天没白折腾。另外我去rolldown-vite
github 官方仓库查看了一下v6的最高版本是v6.3.21
, 按理说,后面的版本是对前面版本的改进和完善, 打包时间应该比前面的版本短,至少持平。 可是我把rolldown-vite
从v6.3.16
换成v6.3.21
之后, 发现打包时间变长了一些54.33s
,猜测可能是增强了兼容性,牺牲了一部分性能。毕竟性能再高, 改造之后项目跑不起来也是白搭。考虑问题的方向没有问题。我还是继续使用6.3.16
, 因为目前项目出现的错误还能hold住。本文完