--- url: /guide/troubleshooting.md --- # 故障排查 ## 性能 性能是 Rolldown 的首要目标之一。然而,构建性能并不仅仅由 Rolldown 本身决定。它还会受到运行环境和所使用插件的显著影响。 虽然我们会持续努力改进 Rolldown,以尽量减少这些外部因素的影响,但仍然存在一些固有的限制,并且某些优化仍在进行中。本指南将介绍可能的瓶颈,以及你可以如何缓解它们。 ### 环境 操作系统及其配置会影响构建时间,尤其是文件系统操作。 #### Windows 与 macOS 或 Linux 等其他操作系统相比,Windows 上的文件系统访问通常更慢。尤其是杀毒软件会让这种情况变得更糟。即使没有杀毒软件干扰,基础文件系统性能通常也更慢。它比 macOS 慢 3 倍,比 Linux 慢 10 倍。当大多数转换都在没有插件的情况下完成时,这就会成为瓶颈。 为了提升 Windows 上的性能,可以考虑使用其他文件系统环境: 1. [**Dev Drive**](https://learn.microsoft.com/en-us/windows/dev-drive/): Windows 较新的一个功能,专为开发者工作负载设计,使用弹性文件系统(ReFS)。与标准的 Windows NTFS 文件系统相比,使用 Dev Drive 进行文件系统操作可带来 **2x 到 3x 的速度提升**。 2. [**Windows Subsystem for Linux (WSL)**](https://learn.microsoft.com/en-us/windows/wsl/): WSL 让 Linux 环境可以轻松在 Windows 上运行,并提供显著更好的文件系统性能。将项目文件放在 WSL 中并在其中运行构建过程,相较于标准的 Windows NTFS 文件系统,文件系统操作的速度可提升约 **10x**。 :::details 基准参考 所使用的基准脚本在这篇博客文章中有描述([How fast can you open 1000 files?](https://lemire.me/blog/2025/03/01/how-fast-can-you-open-1000-files/))。 结果如下: | 文件系统 / 线程数 | 1 | 2 | 4 | 8 | 16 | | -----------------------: | ----: | ----: | ----: | ----: | ----: | | Windows NTFS | 286ms | 153ms | 85ms | 106ms | 110ms | | Windows Dev Drive (ReFS) | 124ms | 67ms | 35ms | 48ms | 55ms | | WSL (ext4) | 24ms | 13ms | 7.8ms | 9.0ms | 13ms | 基准测试运行于以下环境: * 操作系统: Windows 11 Pro 23H2 22631.5189 * CPU: AMD Ryzen 9 5900X * 内存: DDR4-3600 32GB * SSD: Western Digital Black SN850X 1TB ::: ### 插件 插件扩展了 Rolldown 的功能,但也可能引入性能开销。 #### 插件 Hook 过滤器 Rolldown 提供了一项名为 **插件 Hook 过滤器** 的功能。这允许你精确指定插件 hook 应该处理哪些模块,从而减少 JavaScript 和 Rust 之间的通信开销。有关过滤器内部工作原理的详细信息,请参阅 [Hook Filters](/apis/plugin-api/hook-filters) 页面。 如果你是插件使用者,并且你使用的插件没有指定 hook 过滤器,你可以使用 Rolldown 导出的 `withFilter` 工具函数为其添加过滤器。 ```js import yaml from '@rollup/plugin-yaml'; import { defineConfig } from 'rolldown'; import { withFilter } from 'rolldown/filter'; export default defineConfig({ plugins: [ // 仅对以 `.yaml` 结尾的模块运行 `yaml` 插件的 transform hook withFilter(yaml({/*...*/}), { transform: { id: /\.yaml$/ } }), ], }); ``` #### 利用内置功能 Rolldown 包含若干为高效而设计的内置功能。只要可能,优先使用这些原生能力,而不是使用执行类似任务的外部 Rollup 插件。依赖内置功能通常意味着处理完全在 Rust 内部完成,从而可以并行处理。 可查看 [Rolldown Features](/guide/notable-features) 页面,了解 Rollup 中不存在的能力。 例如,以下常见的 Rollup 插件可以被 Rolldown 的内置功能替代: * `@rollup/plugin-alias`: [`resolve.alias`](/reference/InputOptions.resolve#alias) 选项 * `@rollup/plugin-commonjs`: 开箱即支持 * `@rollup/plugin-inject`: [`inject`](/guide/notable-features#inject) 选项 * `@rollup/plugin-replace`: [`replacePlugin`](/builtin-plugins/replace) * `@rollup/plugin-node-resolve`: 开箱即支持 * `@rollup/plugin-json`: 开箱即支持 * `@rollup/plugin-swc`, `@rollup/plugin-babel`, `@rollup/plugin-sucrase`: 通过 Oxc 开箱即支持(复杂配置可能仍然需要插件) * `@rollup/plugin-terser`: `output.minify` 选项 ## 避免直接使用 `eval` `eval()` 函数会对一段 JavaScript 代码字符串求值。`eval()` 调用有两种模式:直接 eval 和间接 eval。直接 eval 指的是直接调用全局 `eval` 函数的情况。与间接 eval 不同,直接 eval 允许传入的字符串访问调用者的局部作用域变量。 在打包代码时,直接 eval 会带来多方面的问题: * Rolldown 采用一种名为“作用域提升(scope hoisting)”的优化,它会把多个文件放入同一个作用域。然而,这意味着通过直接 `eval` 求值的代码可以读取和写入 bundle 中另一个文件里的变量!这会带来正确性问题,因为被求值的代码可能尝试访问一个全局变量,却意外访问到了另一个文件中同名的私有变量。**如果另一个文件中的私有变量包含敏感数据,这甚至可能构成安全问题**。 * Rolldown 可能会重命名 bundle 中的一些变量,以避免名称冲突。虽然在不使用直接 eval 时这不是问题,但对于直接 eval 来说却是问题,因为通过直接 eval 求值的代码可能会尝试使用原始名称引用被重命名后的变量。 * 为了保证正确性,压缩器会避免对可能被直接 eval 代码引用的变量名进行混淆。直接 eval 还会阻止其他一些优化。这意味着输出代码无法被高效压缩。 幸运的是,通常很容易避免使用直接 eval。下面是两种常见的替代方式,它们可以避免上面提到的所有缺点: * `(0, eval)('x')` 这是使用间接 eval 最常见的方式。触发间接 eval 的方法还有其他一些。例如,`var eval2 = eval; eval2('x')`、`[eval][0]('x')` 和 `window.eval('x')` 都属于间接 eval 调用。当你使用间接 eval 时,代码会在全局作用域中求值,而不是在调用者的内联作用域中。 * `new Function('x')` 这会在运行时构造一个新的函数对象。它就像你在全局作用域中写了 `function() { x }` 一样,只不过 `x` 可以是一段任意的代码字符串。这种形式有时很方便,因为你可以给函数添加参数,并使用这些参数向被求值的代码暴露变量。例如,`(new Function('env', 'x'))(someEnv)` 就像你写了 `(function(env) { x })(someEnv)`。当被求值的代码需要访问局部变量时,这通常是直接 `eval` 的一个足够好的替代方案,因为你可以将局部变量作为参数传入。 ## 避免在导出的函数中依赖 `this` 在 JavaScript 中,`this` 是一个特殊变量,它的绑定值通常会根据函数的调用方式而不同。例如,当函数作为对象的方法被调用时,`this` 会绑定到该对象。 ```js const obj = { method() { console.log(this); // 这里的 `this` 是 `obj` }, }; obj.method(); ``` 与此类似,当一个函数从模块中导出,并通过模块命名空间对象调用时,根据 ECMAScript 规范,`this` 会绑定到该模块命名空间对象。 ```js // imported.js export function method() { console.log(this); // 这里的 `this` 是 `imported.js` 的模块命名空间对象 // main.js import * as namespace from './imported.js'; namespace.method(); ``` 然而,在这种情况下,**Rolldown 不一定会保留 `this` 的值**。因此,建议避免在导出的函数中依赖 `this`。不过,这种行为在大多数打包器中都很常见,实际上通常不会成为问题。 之所以会有这种行为,是因为保留 `this` 的值会限制 tree-shaking 的可能性。例如,如果 `this` 变量需要绑定到模块命名空间对象,那么即使模块中的某些导出没有通过 `import` 使用,该模块中的所有导出也都无法被 tree-shake 掉。 ::: tip 输出为 CJS 时的类似问题 与上面描述的问题类似,当将代码输出为 CJS 时,Rolldown 也不一定会保留导出函数的 `this` 值。在这种情况下,本应为 `undefined` 的 `this` 可能会绑定到 `module.exports` 对象。 ::: ## 避免依赖暂时性死区(TDZ)错误 在 ECMAScript 中,`let`、`const` 和 `class` 声明会创建一个绑定,该绑定从其作用域开始时就存在,但在声明本身被求值之前一直处于未初始化状态。在这段窗口期内读取该绑定,即使通过 `typeof`,也会抛出 `ReferenceError`。这段窗口期被称为“暂时性死区(Temporal Dead Zone,TDZ)”。 ```js typeof x; // ReferenceError:无法在初始化之前访问 'x' let x = 1; ``` 然而,出于正确性和性能方面的多种原因,**Rolldown 不一定会保留 TDZ 语义**。依赖 TDZ 访问抛错的代码在打包后的输出中可能表现不同,因此应当避免。 例如,Rolldown 总是会把模块顶层的 `class X {}` 重写为 `var X = class {}`,这样该绑定就可以与其他顶层声明一起提升。结果是在到达声明之前,该绑定会表现为 `undefined`,而不是抛错。将 [`output.topLevelVar`](/reference/OutputOptions.topLevelVar) 设置为 `true` 会把同样的重写扩展到顶层的 `let` 和 `const`。 ```js // 在 ESM 中,这里会抛出 ReferenceError。 // 在 Rolldown 的打包输出中,`typeof X` 的结果会是 `"undefined"`。 console.log(typeof X); class X {} ``` 再举一个例子,Rolldown 可能会在使用处内联导出的 `const` 值,即使跨越了导入循环。当循环导致常量在声明执行之前被读取时,ESM 会抛出错误,但 Rolldown 会改为返回内联后的值。 ::: code-group ```js [entry.js] import './constants.js'; ``` ```js [constants.js] export const foo = 123; export function bar() { return foo; } import './cycle.js'; ``` ```js [cycle.js] import { bar } from './constants.js'; // 在 ESM 中,`bar()` 会抛出 ReferenceError,因为 `foo` 处于 TDZ。 // 在 Rolldown 的打包输出中,`bar()` 会返回 `123`。 console.log(bar()); ``` ::: ## 警告:“Sourcemap 很可能不正确” 如果你为 bundle 生成了 sourcemap([`sourcemap: true`](/reference/OutputOptions.sourcemap) 或 `sourcemap: 'inline'`),但同时使用了一个或多个在转换代码时未为该转换生成 sourcemap 的插件,就会看到这个警告。 通常,插件只有在它本身(而不是 bundle)被配置为 `sourcemap: false` 时才会省略 sourcemap——所以你只需要把它改掉。如果该插件不生成 sourcemap,可以考虑向插件作者提 issue。 ## 错误:`"Cannot find module '@rolldown/binding-...'"` 这个错误意味着 Node.js 找到了 `rolldown` 包,但没有找到与平台相关的原生包。它通常是由一个已知的 npm 可选依赖 bug 引起的([npm/cli#4828](https://github.com/npm/cli/issues/4828));如果你是用 npm 安装的,删除 `node_modules` 和 `package-lock.json` 后重新安装即可修复。 当配置文件位于一个符号链接目录中,而该目录又指向另一个项目时,也可能出现这种情况,例如 Windows 和 WSL 之间共享的目录([#9854](https://github.com/rolldown/rolldown/issues/9854))。Node.js 在解析导入之前会先将配置解析到其真实路径,因此 `import ... from 'rolldown'` 可能会加载到为其他平台安装的 `node_modules`。请将配置文件放在符号链接目录之外,或者在运行时设置 `NODE_OPTIONS=--preserve-symlinks` 环境变量(这与 pnpm 不兼容,因为 pnpm 的 `node_modules` 布局依赖符号链接)。 ## 错误:"Rolldown 发生 panic" {#panic-debug-info} panic 始终是 Rolldown 的 bug。请使用 [panic 报告模板](https://github.com/rolldown/rolldown/issues/new?template=panic_report.yml)提交问题。 发布构建会从发布的 binding 中剥离调试信息,因此回溯中不会显示文件名和行号。设置 `RUST_BACKTRACE=1` 也无法补上这些信息: ```text Rolldown panicked. This is a bug in Rolldown, not your code. thread '' panicked at crates/rolldown/src/some_file.rs:42:5: called `Option::unwrap()` on a `None` value stack backtrace: note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ``` 每个版本都会将 binding 的调试信息作为单独的归档文件附上。将该归档解压到 `.node` 文件所在目录后,回溯便会显示缺失的栈帧。Rust 会自动查找解压后的文件,无需设置其他选项。 以下三个平台提供了调试信息归档: | 平台 | Binding package | 归档文件 | | ----------------- | ---------------------------------- | -------------------------------------------------------- | | Linux x64 (glibc) | `@rolldown/binding-linux-x64-gnu` | `rolldown-binding.linux-x64-gnu.node.debuginfo.tar.zst` | | macOS arm64 | `@rolldown/binding-darwin-arm64` | `rolldown-binding.darwin-arm64.node.debuginfo.tar.zst` | | Windows x64 | `@rolldown/binding-win32-x64-msvc` | `rolldown-binding.win32-x64-msvc.node.debuginfo.tar.zst` | 以下步骤以 macOS arm64 为例。请根据你的平台替换对应名称。 ```sh # 1. Read the installed version. node -p "require('rolldown/package.json').version" # 2. Download the archive from the release with that version. gh release download v1.2.3 --repo rolldown/rolldown \ --pattern 'rolldown-binding.darwin-arm64.node.debuginfo.tar.zst' # 3. Decompress the archive, then unpack it into the binding package. zstd -d rolldown-binding.darwin-arm64.node.debuginfo.tar.zst tar -xf rolldown-binding.darwin-arm64.node.debuginfo.tar \ -C node_modules/@rolldown/binding-darwin-arm64/ # 4. Run the build again. RUST_BACKTRACE=1 npx rolldown -c ``` 第 3 步需要使用 `zstd` 命令。大多数包管理器都提供该工具,例如 `brew install zstd` 或 `apt install zstd`。 你也可以在浏览器中完成第 2 步: 1. 打开[发布页面](https://github.com/rolldown/rolldown/releases)。 2. 找到版本号相同的标签。 3. 从该标签的资源中下载归档文件。 此时每个栈帧都会显示源文件和行号。请将此回溯粘贴到问题报告中: ```text stack backtrace: 0: rust_begin_unwind at /rustc//library/std/src/panicking.rs:679:5 1: core::panicking::panic_fmt at /rustc//library/core/src/panicking.rs:80:14 2: rolldown::some_module::some_function at ./crates/rolldown/src/some_file.rs:42:5 ``` ::: tip 下一次运行 `npm install` 时,binding 包会被替换,已解压的文件也会被删除。每次安装后都需要重新解压该归档。 :::