代码分割
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后便能按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。
为什么要代码分割
如果不进行代码分割,webpack 会将所有的依赖打包进一个巨大的 bundle.js 中,这会导致以下问题:
- 首次加载时间过长
- 缓存失效频率高
代码分割实现了这些功能:
- 按需加载:只有在需要的时候才加载代码,可以减少首次加载时间。
- 并行加载: 将代码拆分成多个小文件,可以利用浏览器的多线程并行下载,提高加载速度。
- 缓存利用:将不同的代码分割到不同的文件中,可以利用浏览器的缓存机制,减少重复加载。
代码分离
常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用分离代码。
方法一:多入口起点-最基础的手动配置
通过配置entry属性,手动指定多个入口起点,每个入口起点会生成一个独立的 bundle 文件。
module.exports = {
entry: {
main: "./src/main.js",
vendor: "./src/vendor.js",
},
output: {
filename: "[name].js",
path: __dirname + "/dist",
},
};
缺点
- 如果入口文件之间共享了相同的模块,那么这些模块会被打包进每个入口的 bundle 中(造成重复代码打包)
- 不够灵活,不能根据实际需要动态分离代码
方法二:防止重复
1.手动指定每个入口的 dependOn(只能共享无副作用的同步代码)
dependOn 允许你显式地声明当前入口文件依赖于其他入口 chunk。这样,Webpack 就不会把依赖的模块重复打包进当前的文件中,而是确保依赖的 chunk 先加载。
假设我们有两个入口 index 和 another,它们都共同使用了 lodash。我们可以把 lodash 单独提取为一个入口 shared,然后让另外两个入口依赖它。
const path = require("path");
module.exports = {
mode: "development",
entry: {
// 1. 先定义一个共享的入口,只包含 lodash
shared: ["lodash"],
// 2. index 入口
index: {
import: "./src/index.js",
// 关键点:告诉 webpack 这个 entry 依赖于 'shared' chunk
dependOn: "shared",
filename: "index.bundle.js",
},
// 3. another 入口
another: {
import: "./src/another-module.js",
dependOn: "shared",
filename: "another.bundle.js",
},
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
};
- 没有 dependOn 时: Webpack 会分析
index.js发现它用了 lodash,于是把 lodash 打包进 index.bundle.js;同理,another.bundle.js 里也会有一份 lodash。这就造成了重复。 - 使用 dependOn 后:
- Webpack 会生成一个 shared.bundle.js,里面包含 lodash。
- index.bundle.js 知道它需要 shared,所以它里面不会再有 lodash 的代码,只会包含加载 shared.bundle.js 的逻辑。
- 当你在 HTML 中引用 index.bundle.js 时,Webpack 的运行时会确保 shared.bundle.js 被先加载。
2.自动代码分离- SplitChunksPlugin
SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件去除之前示例中重复的 lodash 模块:
module.exports = {
// ...
optimization: {
splitChunks: {
// 这里的配置项会覆盖默认配置,所以建议只覆盖需要修改的,或者在下面自定义 chunks
chunks: "async", // (1) 作用范围:只对异步加载的模块生效 (all | initial | async)
minSize: 30000, // (2) 最小尺寸:模块大于 30KB 才会被提取(压缩前)
maxSize: 0, // (3) 最大尺寸:尝试将大于此值的 chunk 拆分成更小的部分(不保证一定成功)
minChunks: 1, // (4) 最小引用次数:模块至少被引用 1 次才提取
maxAsyncRequests: 5, // (5) 按需加载时的最大并行请求数
maxInitialRequests: 3, // (6) 入口点的最大并行请求数
automaticNameDelimiter: "~", // (7) 连接符:生成的文件名中间的符号 (如 vendors~main.js)
name: true, // (8) 命名:true 表示自动生成文件名(基于 cacheGroups),也可以设为函数或字符串
// (9) 缓存组:核心中的核心!SplitChunks 就是根据 cacheGroups 来拆分的
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 下的模块
priority: -10, // 优先级:数字越大优先级越高
name: "vendors", // 强制命名
},
default: {
// 默认缓存组
minChunks: 2, // 至少被 2 个 chunk 共享才提取
priority: -20,
reuseExistingChunk: true, // 如果当前 chunk 已经包含从主 bundle 中拆分出的模块,则重用该模块,而不是重新打包
},
},
},
},
};
关键配置详解
(1) chunks (作用范围) 这是最重要的配置之一,决定了哪些模块会被分析。
- 'initial': 只处理入口文件的同步依赖。
- 'async' (默认): 只处理异步加载的模块(即通过 import() 加载的)。这是为了不增加 HTTP 请求数量,如果只是小模块,不提取反而更好。
- 'all': 同时处理同步和异步模块。推荐在生产环境使用 all,能最大程度利用缓存。
(2) minSize 与 maxSize
- minSize: 如果一个拆分出来的包只有 1KB,提取它还需要发一次 HTTP 请求,得不偿失。所以默认 30KB 以下的模块不拆分。
- maxSize: Webpack 会尝试将大包拆分成不超过 maxSize 的多个小包。这主要用于优化加载速度(大包加载慢,拆分后可以并行加载)。 (9) cacheGroups (缓存组)
- 这是 SplitChunksPlugin 最强大的地方。Webpack 默认有两个组:vendors(第三方库)和 default(公共业务代码)。
匹配规则: 一个模块如果满足多个 cacheGroups 的条件,它会根据 priority(优先级)决定进入哪个组。 继承特性: 如果 cacheGroups 里没有设置 minSize、minChunks 等属性,它们会继承外层的配置。
方法三:动态导入
使用动态导入,webpack 会在构建阶段把动态导入的模块拆分成单独的 chunk,当代码真正执行到这行时,才会去加载这个 chunk
具体流程为
- 当代码执行到动态导入语句时,webpack 会分析这个模块的依赖关系
- 把这个模块及其所有依赖的模块,打包成一个单独的 chunk
- 生成一个对应的 JSONP 函数,用于异步加载这个 chunk
- 当代码真正执行到这行时,会调用 JSONP 函数,触发 chunk 的加载
- 加载完成后,执行这个 chunk 中的代码
例子
// main.js (主文件)
// 绑定一个按钮
document.getElementById("btn").addEventListener("click", () => {
// 只有当用户点击按钮时,才会去加载 heavyMath.js
import("./heavyMath.js").then((module) => {
// 加载成功,执行里面的函数
const doCalc = module.default;
doCalc();
});
});
此时 heavyMath.js 会被拆分成一个单独的 chunk,只有当用户点击按钮时,才会去加载这个 chunk
也就实现了代码分割