CC 4.0 许可证

本节内容摘自以下链接的内容,并受 CC BY 4.0 许可证的约束。

以下内容可以假设为基于原始内容的修改和删除结果,除非另有说明。

代码拆分

Rspack 支持代码拆分,它允许将代码拆分为其他块。您可以完全控制生成资产的大小和数量,从而在加载时间方面获得性能改进。

在这里,我们介绍一个称为“Chunk”的概念,它代表浏览器需要加载的资源。

动态导入

Rspack 使用import() 语法,该语法符合 ECMAScript 对动态导入的提案。

与 webpack 的行为不一致

Rspack 不支持 `require.ensure`。

在 `index.js` 中,我们通过 `import()` 动态导入两个模块,从而将其拆分为一个新的块。

index.js
import('./foo.js');
import('./bar.js');
foo.js
import './shared.js';
console.log('foo.js');
bar.js
import './shared.js';
console.log('bar.js');

现在我们构建这个项目,我们会得到 3 个块,`src_bar_js.js`、`src_foo_js.js` 和 `main.js`,如果你查看它们,你会发现 `shared.js` 存在于 `src_bar_js.js` 和 `src_foo_js.js` 中,我们将在后面的章节中删除重复的模块。

信息

虽然 `shared.js` 存在于 2 个块中,但它只执行一次,您不必担心多个实例的问题。

入口点

这是最简单也是最直观的代码拆分方法。但是,这种方法需要我们手动配置 Rspack。让我们从查看如何从多个入口点拆分多个块开始。

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  stats: 'normal',
};

module.exports = config;
index.js
import './shared';
console.log('index.js');
another-module.js
import './shared';
console.log('another-module');

这将产生以下构建结果

... 资源大小 块 块名称 another.js 1.07 KiB another [已发出] another index.js 1.06 KiB index [已发出] index 入口点 another = another.js 入口点 index = index.js [./src/index.js] 41 字节 {another} {index} [./src/shared.js] 24 字节 {another} {index}

同样,如果你查看它们,你会发现它们都包含重复的 `shared.js`。

SplitChunksPlugin

上面提到的代码分割非常直观,但是大多数现代浏览器都支持并发网络请求。如果我们将 SPA 应用程序的每个页面划分为单个块,并且当用户切换页面时,他们请求一个更大的块,这显然没有很好地利用浏览器处理并发网络请求的能力。因此,我们可以将块分解为更小的块。当我们需要请求此块时,我们改为同时请求这些较小的块,这将使浏览器的请求更有效。

Rspack 默认将 `node_modules` 目录中的文件和重复模块进行拆分,将这些模块从它们的原始块中提取到一个单独的新块中。那么为什么在上面的示例中 `shared.js` 仍然重复出现在多个块中?这是因为我们示例中的 `shared.js` 尺寸非常小。如果将非常小的模块拆分为单独的块供浏览器加载,实际上可能会减慢加载过程。

我们可以将最小拆分大小配置为 0,以允许 `shared.js` 单独提取。

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  entry: {
    index: './src/index.js',
  },
+  optimization: {
+    splitChunks: {
+      minSize: 0,
+    }
+  }
};

module.exports = config;

重建后,你会发现 `shared.js` 已经被单独提取出来了,并且在产品中有一个额外的块包含 `shared.js`。

强制拆分特定模块

我们可以指定某些模块要强制分组到单个块中,例如以下配置

rspack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        test: /\/some-lib\//,
        name: 'lib',
      },
    },
  },
};

使用上述配置,所有路径中包含 `some-lib` 目录的文件都可以提取到名为 `lib` 的单个块中。如果 `some-lib` 中的模块很少更改,那么此块将始终命中用户的浏览器缓存,因此这种经过深思熟虑的配置可以提高缓存命中率。

但是,将 `some-lib` 分割成独立的块也可能存在缺点。假设一个块只依赖于 `some-lib` 中的一个非常小的文件,但是由于 `some-lib` 的所有文件都拆分到一个单独的块中,因此此块必须依赖于整个 `some-lib` 块,导致更大的加载量。因此,在使用 cacheGroups.{cacheGroup}.name 时,需要仔细考虑。

以下示例展示了 cacheGroup 的 `name` 配置的效果。

预取/预加载模块

在声明导入时使用这些内联指令允许 Rspack 输出“资源提示”,告诉浏览器以下内容:

  • 预取:资源可能在未来的某些导航中需要。
  • 预加载:资源在当前导航中也将需要。

一个例子是拥有一个 `HomePage` 组件,它呈现一个 `LoginButton` 组件,然后在被点击后按需加载一个 `LoginModal` 组件。

LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这将导致在页面的头部添加 `<link rel="prefetch" href="login-modal-chunk.js">`,它将指示浏览器在空闲时间预取 `login-modal-chunk.js` 文件。

信息

Rspack 将在父块加载后添加预取提示。

预加载指令与预取相比有一些区别。

  • 预加载块与父块并行开始加载。预取块在父块完成加载后开始。
  • 预加载块具有中等优先级,并且立即下载。预取块在浏览器空闲时下载。
  • 预加载块应由父块立即请求。预取块可以在将来的任何时间使用。
  • 浏览器支持不同。

一个例子是拥有一个 `Component`,它始终依赖于一个应该在单独的块中的大型库。

让我们想象一个 `ChartComponent` 组件,它需要一个巨大的 `ChartingLibrary`。它在渲染时显示一个 `LoadingIndicator`,并立即按需导入 `ChartingLibrary`。

ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');

当请求使用 `ChartComponent` 的页面时,`charting-library-chunk` 也通过 `<link rel="preload">` 被请求。假设页面块更小并且更快地完成,页面将显示一个 `LoadingIndicator`,直到已经请求的 `charting-library-chunk` 完成。这将节省一些加载时间,因为它只需要一次往返而不是两次。尤其是在高延迟环境中。

信息

不正确地使用 webpackPreload 实际上会损害性能,因此在使用它时要小心。

有时您需要对预加载有自己的控制。例如,可以将任何动态导入的预加载通过异步脚本完成。这在流式服务器端渲染的情况下很有用。

const lazyComp = () =>
  import('DynamicComponent').catch(error => {
    // Do something with the error.
    // For example, we can retry the request in case of any net error
  });

如果在 Rspack 开始自行加载脚本之前脚本加载失败(Rspack 创建一个脚本标签来加载其代码,如果该脚本不在页面上),那么 catch 处理程序将不会在 chunkLoadTimeout 超时之前启动。这种行为可能出乎意料。但这是可以解释的——Rspack 无法抛出任何错误,因为 Rspack 不知道脚本失败了。Rspack 将在错误发生后立即为脚本添加 onerror 处理程序。

为了防止出现此问题,您可以添加自己的 onerror 处理程序,以便在出现任何错误时删除脚本。

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

在这种情况下,错误的脚本将被删除。Rspack 将创建自己的脚本,并且任何错误都将在没有超时的情况下被处理。