> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# JavaScript API 架构

Rspack 通过 `@rspack/core` 提供兼容 webpack 的 JavaScript API，而大部分编译工作运行在 Rust 中，两层之间通过 Node-API binding 通信。

## 什么是 native-backed 对象

普通 JavaScript 对象的数据直接保存在 JavaScript 引擎中。例如读取 `{ name: 'main' }` 的 `name` 属性时，JavaScript 可以直接得到结果，不需要调用 Rust。

Rspack 的 `Compilation`、`Module`、`Chunk` 和 graph 等对象有所不同。它们的编译数据主要保存在 Rust 中，JavaScript 对象通常只保存用于找到 Rust 数据的标识或内部句柄；当插件读取某些 getter 或调用 method 时，Rspack 会通过 Node-API binding 访问 Rust，再把结果转换成 JavaScript 值。这类对象称为 **native-backed 对象**。

```text
普通 JavaScript 对象

  读取属性 ──────> JavaScript 对象中的数据 ──────> 直接得到结果


Native-backed 对象

  读取 getter       JavaScript Object       Node-API binding
  或调用 method ──> （标识 / 内部句柄） ──────> （跨语言访问）
                                                    │
                                                    ▼
                                            Rust Compiler / Compilation
                                            （实际编译数据）
                                                    │
                                                    ▼
  得到 JavaScript 值 <────── 数据转换 <──────── Node-API binding
```

两种对象看起来可能使用完全相同的 JavaScript 语法，但行为不同：

| 对象类型             | 数据保存在哪里       | 读取属性或调用方法时                    | 生命周期                          |
| ---------------- | ------------- | ----------------------------- | ----------------------------- |
| 普通 JavaScript 对象 | JavaScript 内存 | 直接读取，不经过 binding              | 只要对象仍被引用，数据通常就存在              |
| Native-backed 对象 | 主要位于 Rust     | 可能通过 Node-API binding 读取或修改数据 | 受对应 compiler 或 compilation 限制 |

一个 native-backed 对象可以同时包含两类属性：

- 字符串、数字等不可变数据可能在创建对象时就复制到 JavaScript，之后读取不需要访问 Rust；
- getter 和 method 可能在每次调用时访问 Rust，以获得当前编译数据或执行操作。

不能只根据 JavaScript 对象是否仍然存在，判断它仍持有创建时的数据。rebuild 后，getter 或 method 可能会从最新的 Rust compilation 重新获取数据，也可能因为目标已经不存在而报错。

这种实现方式带来两个直接影响：

1. **生命周期：**&#x6E;ative-backed 对象的含义通常只有在产生它的 compiler、compilation 或 hook 执行范围内才是可靠的。
2. **性能：**&#x8BFB;取 getter 或调用 method 可能发生 JavaScript 和 Rust 之间的数据转换。在循环中重复访问同一属性，成本会累积。

如果需要在之后使用数据，应在 native-backed 对象所表示的构建仍然活跃时，提取字符串、数字或普通 JavaScript 对象，而不是长期保存这个对象。

## Compiler 生命周期

创建 `@rspack/core` 的 `Compiler` 时，Rspack 不会立即创建对应的 native compiler。配置规范化和插件 `apply` 首先在 JavaScript 侧完成；直到第一次调用 `run()`、`watch()` 等构建 API 时，Rspack 才会按需创建 Rust compiler。

native compiler 创建完成后，下面这些信息已经固定并传递到 Rust：

- 规范化后的配置和 builtin plugin；
- JavaScript hook 与 native hook adapter 之间的注册函数；
- 用于跨语言调用的文件系统和 resolver factory

不要假设 native compiler 创建后，对 `compiler.options` 的任意修改仍会同步到 Rust。配置修改和插件应用应在第一次调用 `run()` 或 `watch()` 前完成。

:::warning 与 webpack 对象模型的差异
同一个 `Compiler` 实例在任意时刻只存在一个活跃的 native `Compilation`。每次构建，包括 watch rebuild，都会替换这个 native compilation。

在开发模式下，一些 webpack 插件会在一次构建的 `done` hook 结束后，继续使用该构建的 JavaScript `Compilation`。这些后续操作执行时，watch rebuild 可能已经开始，从而出现插件持有上一次构建 `Compilation` 的竞态。为了兼容这种常见写法，Rspack 将所有 JavaScript `Compilation` 都作为 compiler **当前** native compilation 的视图。因此，旧对象上的 getter 和 method 可能会读取或修改最新一次构建，而不是报错。但保留这个对象并不能保留上一次构建的数据。

watch 模式下，应使用当前构建 hook 提供的 `Compilation` 和 native-backed 对象。如果需要保存历史数据，应在对应构建期间将其复制为普通 JavaScript 值。
:::

不再使用 compiler 时，应调用 `close()` 并等待 callback 完成。这会等待当前编译结束并释放 compiler 持有的 native 资源和 JavaScript callback。

## Compilation 和对象生命周期

每次构建，包括 watch rebuild，都会创建新的 `Compilation`。从 `Compilation` 获取的 `Module`、`Chunk` 和 graph 等对象，并不会把全部数据复制到 JavaScript。读取其中一些属性或调用方法时，Rspack 仍需要通过 N-API 从当前 Rust compilation 获取数据。

建议遵守以下规则：

- 只在当前构建的 hook 和回调中使用对应的 `Compilation`、`Module`、`Chunk`、graph、dependency 和 block。
- 除非 API 文档明确说明可以长期使用，否则不要把这些对象保留到 hook 结束后或下一次构建。
- 如果稍后仍需要某些信息，应提前保存 identifier、name、path、hash 等字符串或数字，或者转换成普通 JavaScript 对象。
- watch rebuild 后，应从新的 `Compilation` 重新获取对象，不要复用上一次构建保存的对象。

## Hook 和回调执行

Rust compiler 可以使用多个线程，同时执行多个彼此独立的编译任务。但是，JavaScript hook、loader 和其他回调不能直接在这些 Rust 线程上运行，它们必须被调度回创建 Rspack compiler 的 JavaScript 环境。

同一个 JavaScript 环境中的用户代码由一个 JavaScript 线程执行。即使多个 Rust 线程同时触发回调，这些回调也需要先进入队列，再由 JavaScript 线程逐个执行：

```text
Rust 多线程并行执行                 JavaScript 单线程执行

  编译任务 A ─┐
  编译任务 B ─┼─> 跨线程回调队列 ─> 逐个执行回调 ─> 返回结果 ─> 对应 Rust 任务继续
  编译任务 C ─┘
```

hook 和回调的成本不只有 Node-API 数据转换和线程调度。更重要的是，它们会形成一个**串行执行点**：原本可以并行运行的多个 Rust 任务，可能同时等待 JavaScript 队列。回调越频繁、执行时间越长，Rust 并行带来的收益就越容易被抵消。回调之前和之后的 Rust 工作仍然可以并行，但依赖 JavaScript 结果的这一段会受到单线程吞吐量限制。

### Loader 为什么尤其容易成为瓶颈

Rspack 可以同时处理多个文件，但 JavaScript loader 需要在 JavaScript 线程上执行。当多个文件同时等待 loader 处理时，对应的 JavaScript loader 会排队；等待 loader 结果的 Rust 编译任务也会停在这里。因此，针对每个文件调用的 JavaScript loader 越重、调用次数越多，整体构建就越接近串行执行。

如果功能相同，应优先使用 Rspack 的 builtin loader。例如：

- 使用 [`builtin:swc-loader`](/zh/guide/features/builtin-swc-loader.md) 执行 JavaScript 和
  TypeScript 转换；
- 使用 [`builtin:lightningcss-loader`](/zh/guide/features/builtin-lightningcss-loader.md) 执行
  CSS 转换。

Builtin loader 可以在 Rust 侧执行，减少 JavaScript 排队和跨语言数据转换，并更好地利用 Rspack 的并行能力。

## 高效用法

### 只读取需要的资源

`compilation.assets` 看起来像普通 JavaScript 对象，但它的资源内容由 native compilation 按需提供。`Object.keys(assets)` 只获取资源名称；读取 `assets[name]` 时，Rspack 才会通过 binding 从 Rust 获取对应的 `Source`。

如果只需要部分资源，下面的写法效率较低：

```js title="低效写法"
compilation.hooks.processAssets.tap('ExamplePlugin', (assets) => {
  for (const [name, source] of Object.entries(assets)) {
    if (name.endsWith('.js')) {
      consumeJavaScriptAsset(name, source);
    }
  }
});
```

`Object.entries(assets)` 会在执行循环前读取所有资源的 `Source`。即使循环最终只使用少量资源，
也会产生不必要的跨语言通信和内存开销。

应该先使用 `Object.keys()` 筛选资源名称，再读取需要的 `Source`：

```js title="推荐写法"
compilation.hooks.processAssets.tap('ExamplePlugin', (assets) => {
  for (const name of Object.keys(assets)) {
    if (!name.endsWith('.js')) {
      continue;
    }

    const source = assets[name];
    consumeJavaScriptAsset(name, source);
  }
});
```

如果确实需要读取所有资源，使用 `Object.entries()` 或 `Object.values()` 不会造成额外的无效读取。

### 在循环中只读取一次

对于需要从 Rust 获取的数据，应在一次遍历中读取并保存结果：

```js
compilation.hooks.processAssets.tap('ExamplePlugin', () => {
  const moduleInfo = [];

  for (const module of compilation.modules) {
    const size = module.size();
    moduleInfo.push({
      identifier: module.identifier(),
      size,
    });
  }

  consumeModuleInfo(moduleInfo);
});
```

`module.size()` 需要通过 binding 从 Rust 获取数据。将 `size` 保存在局部变量中，可以避免后续逻辑重复调用 `module.size()`。
