Skip to content

为什么要使用插件钩子过滤器?

¥Why Plugin Hook Filters?

问题

¥The Problem

即使 Rolldown 的核心是用 Rust 编写的,具有并行处理能力,添加 JavaScript 插件也会显著降低构建速度。为什么?因为每个插件钩子都会被每个模块调用,即使插件并不关心其中的大多数模块。

¥Even though Rolldown's core is written in Rust with parallel processing capabilities, adding JavaScript plugins can significantly slow down your builds. Why? Because each plugin hook gets called for every module, even when the plugin doesn't care about most of them.

例如,如果你有一个仅转换 .css 文件的 CSS 插件,它仍然会被项目中的每个 .js.ts.jsx 和其他文件调用。如果插件数量为 10 个,则开销会成倍增加,导致构建时间增加 3-4 倍。

¥For example, if you have a CSS plugin that only transforms .css files, it still gets called for every .js, .ts, .jsx, and other file in your project. With 10 plugins, this overhead multiplies, causing build times to increase by 3-4x.

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

¥Plugin hook filters solve this by letting Rolldown skip unnecessary plugin calls at the Rust level, keeping your builds fast even with many plugins.

实际影响

¥Real-World Impact

让我们使用 apps/10000 进行基准测试,看看实际的性能差异:分支:https://github.com/rolldown/benchmarks/pull/3

¥Let's see the actual performance difference with a benchmark using apps/10000: branch: 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>

设置:

¥Setup:

  • 10 个 CSS 文件(foo1.cssfoo10.css

    ¥10 CSS files (foo1.css to foo10.css)

  • 每个插件只转换一个特定的 CSS 文件(例如,插件 1 只关注 foo1.css

    ¥Each plugin transforms only one specific CSS file (e.g., plugin 1 only cares about foo1.css)

  • 通过 PLUGIN_COUNT 控制可变数量的插件

    ¥Variable number of plugins controlled via PLUGIN_COUNT

  • 使用标准模式的插件:检查文件是否匹配,如果不匹配则提前返回

    ¥Plugins use standard pattern: check if file matches, return early if not

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

¥Without Filter (Traditional Approach)

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.8 秒 vs 745 毫秒)。

¥Key Takeaway: Build time scales linearly with plugin count - 10 plugins = 3.74x slower (2.8s vs 745ms).

解决方案:插件钩子过滤器

¥The Solution: Plugin Hook Filters

无需为每个模块调用每个插件,而是使用 filter 来告诉 Rolldown 每个插件关注哪些文件。操作方法:

¥Instead of calling every plugin for every module, use filter to tell Rolldown which files each plugin cares about. Here's how:

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,
+          };
+        }
       }
     }
   }

更改内容:

¥What changed:

  • transform 函数封装在具有 handlerfilter 属性的对象中

    ¥Wrapped the transform function in an object with handler and filter properties

  • 添加了 filter.id.include,其正则表达式模式仅匹配此插件关注的文件。

    ¥Added filter.id.include with a regex pattern matching only the files this plugin cares about

  • Rolldown 现在会在调用 JavaScript 之前检查 Rust 中的过滤器。

    ¥Rolldown now checks the filter in Rust before calling into JavaScript

使用过滤器(已优化)

¥With Filter (Optimized)

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'

关键要点:使用过滤器后,所有插件的执行时间几乎相同(约 740 毫秒)。开销已被消除。

¥Key Takeaway: With filters, all plugin counts perform nearly identically (~740ms). The overhead has been eliminated.

性能对比

¥Performance Comparison

插件数量禁用过滤器启用过滤器加速
0 个插件745 毫秒739 毫秒1.0x
1 个插件863 毫秒761 毫秒1.13x
2 个插件1,106 毫秒731 毫秒1.51x
5 插件1,848 毫秒742 毫秒2.49x
10 个插件2,792 毫秒747 毫秒3.74x

总结:如果你的插件只关心特定文件,那么无论添加多少个插件,都可以使用过滤器来保持快速的构建时间。

¥Bottom line: When you have plugins that only care about specific files, use filters to maintain fast build times regardless of how many plugins you add.

底层工作原理

¥How It Works Under the Hood

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

¥To understand why filters are so effective, you need to understand how Rolldown processes modules with JavaScript plugins.

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

¥Rolldown uses parallel processing (like the producer-consumer problem) to build the module graph efficiently. Here's a simple dependency graph to illustrate:

依赖图

¥Dependency Graph

dependency graph

不使用 JavaScript 插件

¥Without JavaScript Plugins

Bundling without JavaScript plugins

Rust 中的所有内容都是并行运行的。多个 CPU 核心同时处理模块,最大化吞吐量。

¥Everything runs in parallel in Rust. Multiple CPU cores process modules simultaneously, maximizing throughput.

这些图表显示的是概念算法,而非确切的实现细节。为了清晰起见,某些时间片被夸大了 - `fetch_module` 实际上以宏秒级的速度运行。

¥[!NOTE] These diagrams show the conceptual algorithm, not exact implementation details. Some time slices are exaggerated for clarity—fetch_module actually runs at macrosecond speeds.

使用 JavaScript 插件(无过滤器)

¥With JavaScript Plugins (No Filter)

Bundling with JavaScript plugins

瓶颈:JavaScript 插件在单线程中运行。即使 Rolldown 的 Rust 核心是并行的,每个模块也必须:

¥Here's the bottleneck: JavaScript plugins run in a single thread. Even though Rolldown's Rust core is parallel, every module must:

  1. 在 "diamond"(钩子调用阶段)停止

    ¥Stop at the "diamond" (hook call phase)

  2. 从 Rust 跨越 FFI 边界到 JavaScript

    ¥Cross the FFI boundary from Rust → JavaScript

  3. 等待所有插件串行运行

    ¥Wait for all plugins to run serially

  4. 从 JavaScript 跨回 Rust

    ¥Cross back from JavaScript → Rust

这个序列化点会成为一个主要瓶颈。请注意,随着更多插件的添加,菱形部分如何变宽,而 CPU 核心则处于空闲状态,等待 JavaScript。

¥This serialization point becomes a major bottleneck. Notice how the diamond section grows wider as more plugins are added, while CPU cores sit idle waiting for JavaScript.

使用过滤器(已优化)

¥With Filters (Optimized)

添加过滤器时,Rolldown 会在进入 JavaScript 之前先在 Rust 中对其进行评估:

¥When you add filters, Rolldown evaluates them in Rust before crossing into JavaScript:

For each module:
  For each plugin:
    ✓ Check filter in Rust (macrosecond)
    ✗ Skip if no match
    → Only call JavaScript for matching plugins

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

¥This eliminates the majority of FFI overhead and JavaScript execution time. In the benchmark, most plugins don't match most files, so nearly all calls are skipped. The diamond shrinks back down, CPU utilization stays high, and build times remain fast.

何时使用过滤器

¥When to Use Filters

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

¥Use filters when:

  • ✅ 你的插件仅处理特定文件类型(例如,.css.svg.md

    ¥✅ Your plugin only processes specific file types (e.g., .css, .svg, .md)

  • ✅ 你的插件针对特定目录(例如,src/**node_modules/**

    ¥✅ Your plugin targets specific directories (e.g., src/**, node_modules/**)

  • ✅ 你的构建中有多个插件

    ¥✅ You have multiple plugins in your build

  • ✅ 你关心构建性能

    ¥✅ You care about build performance

快速参考

¥Quick Reference

js
// ❌ Without filter - called for every module
export default {
  name: 'my-plugin',
  transform(code, id) {
    if (!id.endsWith('.css')) return;
    // ... transform CSS
  },
};

// ✅ With filter - only called for CSS files
export default {
  name: 'my-plugin',
  transform: {
    filter: {
      id: { include: /\.css$/ },
    },
    handler(code, id) {
      // ... transform CSS
    },
  },
};

有关完整的过滤器 API 和选项,请参阅 插件钩子过滤器的使用

¥See the plugin hook filter usage for complete filter api and options.