CC 4.0 协议声明
本节内容派生于以下链接指向的内容 ,并遵守 CC BY 4.0 许可证的规定。
以下内容如果没有特殊声明,可以认为都是基于原内容的修改和删减后的结果。
代码分割
Rspack 支持代码分割特性,允许让你对代码进行分割,控制生成的资源体积来获取资源加载性能的提升。
常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用来分离代码。
入口起点(entry point)
这是最简单直观分离代码的方式。但这种方式需要我们手动对 Rspack 进行配置,并暗藏一些隐患,我们将会解决这些问题。我们先来看看如何从通过多个入口起点分割出多个 Chunk 。
rspack.config.js
/**
* @type {import('@rspack/core').Configuration}
*/
const config = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
},
stats: 'normal',
};
module.exports = config;
index.js
import './shared';
console.log('index.js');
another-module.js
import './shared';
console.log('another-module');
这将生成如下构建结果:
...
Asset Size Chunks Chunk Names
another.bundle.js 1.07 KiB another [emitted] another
index.bundle.js 1.06 KiB index [emitted] index
Entrypoint another = another.bundle.js
Entrypoint index = index.bundle.js
[./src/index.js] 41 bytes {another} {index}
[./src/shared.js] 24 bytes {another} {index}
正如前面提到的,这种方式存在一些隐患:
- 如果多个入口起点导入链之间包含一些重复的模块,那么这些重复模块会被重复添加到各个入口 Chunk 中。
shared.js
的代码会被同时打包到 index.bundle.js
和 another.bundle.js
中。
- 这种方法不够灵活,并且不能动态地将程序逻辑中的代码拆分出来。
以上两点中,第一点对我们的示例来说无疑是个问题,在下一章节我们会讲述如何移除重复的模块。
SplitChunksPlugin
SplitChunksPlugin 插件可以将公共的依赖模块提提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 shared.js
模块去除:
rspack.config.js
/**
* @type {import('@rspack/core').Configuration}
*/
const config = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ minSize: 1,
+ }
+ }
};
module.exports = config;
使用 optimization.splitChunks
配置选项之后,现在应该可以看出,index.bundle.js
和 another.bundle.js
中已经移除了重复的依赖模块。需要注意的是,插件将 shared.js
分离到单独的 another~index.bundle.js
中 ,并且将其从 index.bundle.js
和 another.bundle.js
中移除。
构建结果:
Asset Size Chunks Chunk Names
another.bundle.js 3.27 KiB another [emitted] another
index.bundle.js 3.27 KiB index [emitted] index
+ another~index.bundle.js 462 bytes another~index [emitted]
Entrypoint another = another.bundle.js another~index.bundle.js
Entrypoint index = another~index.bundle.js index.bundle.js
[./src/index.js] 41 bytes {another~index}
[./src/shared.js] 24 bytes {another~index}
动态导入(dynamic import)
当涉及到动态代码拆分时, Rspack 选择的方式是使用符合 ECMAScript 提案 的 import()
语法来实现动态导入。
Webpack 的差异点
- Rspack 不支持
require.ensure
功能。
在我们开始之前,先从上述示例的配置中移除掉多余的 entry 和 optimization.splitChunks,因为接下来的演示中并不需要它们:
rspack.config.js
/**
* @type {import('@rspack/core').Configuration}
*/
const config = {
mode: 'development',
entry: {
'index': './src/index.js',
- 'another': './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- minSize: 1,
- }
- }
};
module.exports = config;
现在,我们将不在 index.js
中静态导入 shared.js
,而是通过 import()
来动态导入它,从而分离出一个新的 chunk:
index.js
- import './shared'
+ import('./shared')
console.log('index.js')
让我们执行 rspack build
看看, shared.js
被单独分割到 src_shared_js.bundle.js
中了。
...
Asset Size Chunks Chunk Names
index.bundle.js 12.9 KiB index [emitted] index
+ src_shared_js.bundle.js 245 bytes src_shared_js [emitted]
Entrypoint index = index.bundle.js
[./src/index.js] 42 bytes {index}
[./src/shared.js] 24 bytes {src_shared_js}
build: 67.303ms
Prefetching/Preloading 模块
声明 import
时使用下列内置指令可以让 Rspack 产出标签以触发浏览器:
- 预取(prefetch): 将来某些导航下可能需要的资源
- 预载(preload): 当前导航下可能需要资源
试想一下下面的场景:现有一个 HomePage
组件,其内部渲染了一个 LoginButton
组件,点击该按钮后按需加载 LoginModal
组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
上面的代码在构建时会生成 <link rel="prefetch" href="login-modal-chunk.js">
并添加到页面头部,以此触发浏览器在空闲时预取 login-modal-chunk.js
文件。
INFO
Rspack 将在父 chunk 加载后添加预取标签。
预载与预取有如下不同点:
- 预载 chunk 与父 chunk 同时并行加载,而预取 chunk 则在父 chunk 加载结束后开始加载。
- 预载 chunk 具有中等优先级并立即加载,而预取 chunk 则需要等待浏览器空闲
- 预载 chunk 会在父 chunk 中立即请求,而预取 chunk 则会在未来某个时间点被使用
- 浏览器支持程度不同
如以下示例,一个 Component 依赖一个大型库,该库被拆分到了一个独立 chunk 中
假设一个 ChartComponent
组件 需要一个大型 ChartingLibrary
库。它会在渲染时显示一个 LoadingIndicator
组件,然后立即按需引入 ChartingLibrary
:
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
当请求使用 ChartComponent
的页面时,也会通过 <link rel="preload">
请求 charting-library-chunk
。假设 page-chunk
较小且完成得更快,页面将显示LoadingIndicator
,直到已请求的 charting-library-chunk
完成。这将带来一点加载时间的提升,因为它只需要一次往返而不是两次。尤其是在高延迟环境中。
INFO
错误地使用 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 创建一个 script 标签来加载代码),则该异常将无法被捕获,直到 chunkLoadTimeout 超时。这可能出乎预料但可解释为 —— Rspack 无法抛出任何异常,因为 Rspack 并不知道该脚本失败了。Rspack 将在错误发生后立即为 script 标签添加 onerror 监听。
为了避免发生这类问题,您可以添加自己的 onerror 监听,在发生异常时删除该 script:
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
在这个示例中,发生错误的 script 将被移除。Rspack 会创建自己的 script 并在超时前处理任何发生的异常。