Skip to content

为什么需要插件 Hook 过滤器?

问题

尽管 Rolldown 的核心是用 Rust 编写的,并且具备并行处理能力,添加 JavaScript 插件仍然会显著拖慢构建速度。为什么?因为每个插件 hook 都会针对 每个 模块被调用一次,即使插件并不关心其中大多数模块。

例如,如果你有一个只处理 .css 文件的 CSS 插件,它仍然会对项目中的每个 .js.ts.jsx 以及其他文件被调用。随着插件数量增加到 10 个,这种开销会成倍增长,导致构建时间增加 3-4 倍

插件 hook 过滤器通过让 Rolldown 在 Rust 层跳过不必要的插件调用来解决这个问题,即使有很多插件,也能保持构建速度。

真实影响

让我们通过一个使用 apps/10000 的基准测试来看实际的性能差异: 分支:https://github.com/rolldown/benchmarks/pull/3

diff
diff --git a/apps/10000/rolldown.config.mjs b/apps/10000/rolldown.config.mjs
--- a/apps/10000/rolldown.config.mjs
+++ b/apps/10000/rolldown.config.mjs
@@ -1,8 +1,25 @@
 import { defineConfig } from "rolldown";
-import { minify } from "rollup-plugin-esbuild";
+// import { minify } from "rollup-plugin-esbuild";
 const sourceMap = !!process.env.SOURCE_MAP;
 const m = !!process.env.MINIFY;
+const transformPluginCount = process.env.PLUGIN_COUNT || 0;

+let transformCssPlugin = Array.from({ length: transformPluginCount }, (_, i) => {
+  let index = i + 1;
+  return {
+    name: `transform-css-${index}`,
+    transform(code, id) {
+      if (id.endsWith(`foo${index}.css`)) {
+        return {
+          code: `.index-${index} {
+  color: red;
+}`,
+          map: null,
+        };
+      }
+    }
+  }
+})
 export default defineConfig({
 	input: {
 		main: "./src/index.jsx",
@@ -11,13 +28,7 @@ export default defineConfig({
 		"process.env.NODE_ENV": JSON.stringify("production"),
 	},
 	plugins: [
-		m
-			? minify({
-					minify: true,
-					legalComments: "none",
-					target: "es2022",
-				})
-			: null,
+    ...transformCssPlugin,
 	].filter(Boolean),
 	profilerNames: !m,
 	output: {
diff --git a/apps/10000/src/index.css b/apps/10000/src/index.css
deleted file mode 100644
diff --git a/apps/10000/src/index.jsx b/apps/10000/src/index.jsx
--- a/apps/10000/src/index.jsx
+++ b/apps/10000/src/index.jsx
@@ -1,7 +1,16 @@
 import React from "react";
 import ReactDom from "react-dom/client";
 import App1 from "./f0";
-import './index.css'
+import './foo1.css'
+import './foo2.css'
+import './foo3.css'
+import './foo4.css'
+import './foo5.css'
+import './foo6.css'
+import './foo7.css'
+import './foo8.css'
+import './foo9.css'
+import './foo10.css'

 ReactDom.createRoot(document.getElementById("root")).render(
 	<React.StrictMode>

设置:

  • 10 个 CSS 文件(foo1.cssfoo10.css
  • 每个插件只转换一个特定的 CSS 文件(例如,插件 1 只关心 foo1.css
  • 通过 PLUGIN_COUNT 控制插件数量
  • 插件使用标准模式:检查文件是否匹配,不匹配则提前返回

不使用过滤器(传统方式)

bash
Benchmark 1: PLUGIN_COUNT=0 node --run build:rolldown
  Time (mean ± σ):     745.6 ms ±  11.8 ms    [User: 2298.0 ms, System: 1161.3 ms]
  Range (min  max):   732.1 ms … 753.6 ms    3 runs

Benchmark 2: PLUGIN_COUNT=1 node --run build:rolldown
  Time (mean ± σ):     862.6 ms ±  61.3 ms    [User: 2714.1 ms, System: 1192.6 ms]
  Range (min  max):   808.3 ms … 929.2 ms    3 runs

Benchmark 3: PLUGIN_COUNT=2 node --run build:rolldown
  Time (mean ± σ):      1.106 s ±  0.020 s    [User: 3.287 s, System: 1.382 s]
  Range (min  max):    1.091 s …  1.130 s    3 runs

Benchmark 4: PLUGIN_COUNT=5 node --run build:rolldown
  Time (mean ± σ):      1.848 s ±  0.022 s    [User: 4.398 s, System: 1.728 s]
  Range (min  max):    1.825 s …  1.869 s    3 runs

Benchmark 5: PLUGIN_COUNT=10 node --run build:rolldown
  Time (mean ± σ):      2.792 s ±  0.065 s    [User: 6.013 s, System: 2.198 s]
  Range (min  max):    2.722 s … 2.850 s    3 runs

Summary
 'PLUGIN_COUNT=0 node --run build:rolldown' ran
    1.16 ± 0.08 times faster than 'PLUGIN_COUNT=1 node --run build:rolldown'
    1.48 ± 0.04 times faster than 'PLUGIN_COUNT=2 node --run build:rolldown'
    2.48 ± 0.05 times faster than 'PLUGIN_COUNT=5 node --run build:rolldown'
    3.74 ± 0.10 times faster than 'PLUGIN_COUNT=10 node --run build:rolldown'

关键结论: 构建时间会随着插件数量线性增长——10 个插件会慢 3.74 倍(2.8s 对比 745ms)。

解决方案:插件 Hook 过滤器

不要对每个模块都调用每个插件,而是使用 filter 告诉 Rolldown 每个插件关心哪些文件。方法如下:

diff
diff --git a/apps/10000/rolldown.config.mjs b/apps/10000/rolldown.config.mjs
index 822af995..dee07e68 100644
--- a/apps/10000/rolldown.config.mjs
+++ b/apps/10000/rolldown.config.mjs
@@ -8,14 +8,21 @@ let transformCssPlugin = Array.from({ length: transformPluginCount }, (_, i) =>
   let index = i + 1;
   return {
     name: `transform-css-${index}`,
-    transform(code, id) {
-      if (id.endsWith(`foo${index}.css`)) {
-        return {
-          code: `.index-${index} {
+    transform: {
+      filter: {
+        id: {
+          include: new RegExp(`foo${index}.css$`),
+        }
+      },
+      handler(code, id) {
+        if (id.endsWith(`foo${index}.css`)) {
+          return {
+            code: `.index-${index} {
   color: red;
 }`,
-          map: null,
-        };
+            map: null,
+          };
+        }
       }
     }
   }

发生了什么变化:

  • transform 函数包装到一个带有 handlerfilter 属性的对象中
  • 添加了 filter.id.include,使用正则表达式匹配该插件关心的文件
  • Rolldown 现在会在进入 JavaScript 之前,先在 Rust 中检查过滤器

使用过滤器(优化后)

bash
Benchmark 1: PLUGIN_COUNT=0 node --run build:rolldown
  Time (mean ± σ):     739.1 ms ±   6.8 ms    [User: 2312.5 ms, System: 1153.0 ms]
  Range (min  max):   733.0 ms … 746.5 ms    3 runs

Benchmark 2: PLUGIN_COUNT=1 node --run build:rolldown
  Time (mean ± σ):     760.6 ms ±  18.3 ms    [User: 2422.1 ms, System: 1107.4 ms]
  Range (min  max):   739.7 ms … 773.6 ms    3 runs

Benchmark 3: PLUGIN_COUNT=2 node --run build:rolldown
  Time (mean ± σ):     731.2 ms ±  11.1 ms    [User: 2461.3 ms, System: 1141.4 ms]
  Range (min  max):   723.9 ms … 744.0 ms    3 runs

Benchmark 4: PLUGIN_COUNT=5 node --run build:rolldown
  Time (mean ± σ):     741.5 ms ±   9.3 ms    [User: 2621.6 ms, System: 1111.3 ms]
  Range (min  max):   734.0 ms … 751.9 ms    3 runs

Benchmark 5: PLUGIN_COUNT=10 node --run build:rolldown
  Time (mean ± σ):     747.3 ms ±   2.1 ms    [User: 2900.9 ms, System: 1120.0 ms]
  Range (min  max):   745.0 ms … 749.2 ms    3 runs

Summary
  'PLUGIN_COUNT=2 node --run build:rolldown' ran
    1.01 ± 0.02 times faster than 'PLUGIN_COUNT=0 node --run build:rolldown'
    1.01 ± 0.02 times faster than 'PLUGIN_COUNT=5 node --run build:rolldown'
    1.02 ± 0.02 times faster than 'PLUGIN_COUNT=10 node --run build:rolldown'
    1.04 ± 0.03 times faster than 'PLUGIN_COUNT=1 node --run build:rolldown'

关键结论: 使用过滤器后,所有插件数量的性能几乎一致(约 740ms)。开销已经被消除了。

性能对比

插件数量不使用过滤器使用过滤器加速比
0 个插件745ms739ms1.0x
1 个插件863ms761ms1.13x
2 个插件1,106ms731ms1.51x
5 个插件1,848ms742ms2.49x
10 个插件2,792ms747ms3.74x

一句话总结: 当你的插件只关心特定文件时,请使用过滤器,以便在增加插件数量时仍能保持快速构建。

底层工作原理

要理解为什么过滤器如此有效,你需要了解 Rolldown 如何使用 JavaScript 插件处理模块。

Rolldown 使用并行处理(类似于 生产者-消费者问题)来高效构建模块图。下面是一个简单的依赖图来说明:

依赖图

aa.jsbb.jsa->bcc.jsa->cdd.jsb->dee.jsb->eff.jsc->f
aa.jsbb.jsa->bcc.jsa->cdd.jsb->dee.jsb->eff.jsc->f

没有 JavaScript 插件

没有 JavaScript 插件时的打包

所有任务都在 Rust 中并行运行。多个 CPU 核心同时处理模块,最大化吞吐量。

NOTE

这些图展示的是概念性算法,而不是精确的实现细节。为便于说明,有些时间片被夸大了——fetch_module 实际上运行速度是微秒级的。

有 JavaScript 插件(无过滤器)

有 JavaScript 插件时的打包

瓶颈在这里:JavaScript 插件在单线程中运行。尽管 Rolldown 的 Rust 核心是并行的,但每个模块都必须:

  1. 在“菱形”处停下来(hook 调用阶段)
  2. 跨越 Rust → JavaScript 的 FFI 边界
  3. 等待 所有 插件串行执行
  4. 再从 JavaScript → Rust 返回

这个串行化点会成为一个主要瓶颈。注意随着插件数量增加,菱形区域会变得更宽,而 CPU 核心则在等待 JavaScript 时处于空闲状态。

使用过滤器(优化后)

添加过滤器后,Rolldown 会在跨入 JavaScript 之前,先在 Rust 中 计算过滤条件:

对于每个模块:
  对于每个插件:
    ✓ 在 Rust 中检查过滤器(微秒级)
    ✗ 如果不匹配则跳过
    → 只为匹配的插件调用 JavaScript

这消除了大部分 FFI 开销和 JavaScript 执行时间。在基准测试中,大多数插件并不匹配大多数文件,因此几乎所有调用都被跳过了。菱形区域缩小了,CPU 利用率保持在高位,构建时间依然很快。

何时使用过滤器

在以下情况下使用过滤器:

  • ✅ 你的插件只处理特定文件类型(例如 .css.svg.md
  • ✅ 你的插件针对特定目录(例如 src/**node_modules/**
  • ✅ 你的构建中有多个插件
  • ✅ 你关注构建性能

快速参考

js
// ❌ 没有过滤器 - 对每个模块都会调用
export default {
  name: 'my-plugin',
  transform(code, id) {
    if (!id.endsWith('.css')) return;
    // ... 转换 CSS
  },
};

// ✅ 使用过滤器 - 仅对 CSS 文件调用
export default {
  name: 'my-plugin',
  transform: {
    filter: {
      id: { include: /\.css$/ },
    },
    handler(code, id) {
      // ... 转换 CSS
    },
  },
};

查看 插件钩子过滤器用法 以获取完整的过滤器 API 和选项。