When ?什么时候进行处理
首先我们要清楚 SplitChunksPlugin 插件侵入 webpack 流程的时机,不然不能很好地理解它的配置参数到底起什么作用。通过源码得知它是在Compilation
的optimizeChunksAdvanced
钩子触发时进行工作的,而此时 chunk 已经生成并完成了初步优化。
【webpack 之 Chunk】中,我们已经分析过这时 chunk 分包的状态:
- 同一个 entry 入口模块与它的同步依赖(直接/间接) 组织成一个 chunk,还包含 runtime (webpackBootstrap 自执行函数的形式)。
- 每一个异步模块与它的同步依赖单独组成一个 chunk。其中只会包含入口 chunk 中不存在的同步依赖;若存在同步第三方包,也会被单独打包成一个 chunk。
那么,SplitChunksPlugin 就是在这个基础上再做优化了,也就是对这些 chunk 进行进一步的组合/分割。
Why and How ?为何要代码分割及如何分割
Code Splitting 拆包优化的最终目标是什么?无非是:
- 把更新频率低的代码和内容频繁变动的代码分离,把共用率较高的资源也拆出来,最大限度利用浏览器缓存。
- 减少 http 请求次数的同时避免单个文件太大以免拖垮响应速度,也就是拆包时尽量实现文件个数更少、单个文件体积更小。
第二点的两个目标是互相矛盾的,因此要达到两者之间的平衡是个博弈的过程,没有太绝对的拆包策略,都是力求提高性能水准罢了。
具体来说,比如一些第三方插件,更新频率其实很低,单个体积通常又较小,就很适合打包在一个文件里。而 UI 组件库更新少的同时体积却比较大,就可以单独打成一个包(也有直接用 CDN 外链的)。还有程序员自己写的公共组件,一般写完后修改也不多,适合拎出来放一个文件。
webpack 配置
output.filename
或output.chunkFilename
值中的[contenthash]
使得重新打包时若 chunk 内容没有变化,就跳过直接使用缓存,当然对应的输出文件名称中的 hash 值也不会改变。这样既能提高二次构建速度,又能不影响用户的浏览器缓存。
为何配置文件名时在取值占位符里使用[contenthash]
而不是[chunkhash]
呢?
[chunkhash]
是 chunk 级别的 hash 值。但在项目中我们通常的做法是把 css 都抽离出来,作为模块import
到对应的 js 文件中。如果使用[chunkhash]
,两个关联的 js 和 css 文件名的 hash 值是一样的。一旦其中一个改动了,与其关联的另一个文件即使毫无变化,文件名也会改变,缓存也就失效了。
而[contenthash]
,只关注文件本身,自身内容不变,hash 值也不会变。
其余剩下的基本就是我们的业务代码,改动频率就很大了,是每次发布版本都会变的。
常用的代码分离方法
- 入口起点:通过 entry 配置手动地分离代码。
- 防止重复:使用 Entry dependencies 或者 内置插件 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过异步引入模块(如
import('./m.js')
)来分离代码。
webpackChunkName
异步加载的 chunk 无法通过 webpack 配置自定义打包后的名称,默认都是以0、1、2...
这样的数字命名。
魔法注释可以帮助我们自定义异步 chunk 名。
component: () => import(/* webpackChunkName: "route-login" */ '@/views/login')
如果想把某个路由下的所有组件都打包在同一个异步块 (chunk) 中。那么在webpackChunkName
注释提供相同的 chunk name 即可。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
SplitChunksPlugin 配置详解
先看下SplitChunksPlugin插件的默认配置,再结合实例来搞懂每个配置项真正的用处。
webpack 上的文档地址:【SplitChunksPlugin API】
module.exports = {
// 加上入口和输出的配置,以便结合实际说明
entry: {
index: './src/a',
admin: './src/b'
},
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash:6].js',
chunkFilename: '[name].[contenthash:8].js',
},
optimization: {
splitChunks: {
chunks: 'async', // 2. 处理的 chunk 类型
minSize: 20000, // 4. 允许新拆出 chunk 的最小体积
minRemainingSize: 0,
minChunks: 1, // 5. 拆分前被 chunk 公用的最小次数
maxAsyncRequests: 30, // 7. 每个异步加载模块最多能被拆分的数量
maxInitialRequests: 30, // 6. 每个入口和它的同步依赖最多能被拆分的数量
enforceSizeThreshold: 50000, // 8. 强制执行拆分的体积阈值并忽略其他限制
cacheGroups: { // 1. 缓存组
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 1.1 模块路径/文件名匹配正则
priority: -10, // 1.2 缓存组权重
reuseExistingChunk: true, // 1.3 复用已被拆出的依赖模块,而不是继续包含在该组一起生成
},
default: {
minChunks: 2, // 5. default 组的模块必须至少被 2 个 chunk 共用 (本次分割前)
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
1. cacheGroups
核心配置 - 缓存组,可以继承/覆盖来自splitChunks.*
的任何选项。它自身拥有test
、priority
和 reuseExistingChunk
三个配置项。
SplitChunksPlugin 就是根据cacheGroups
去拆分模块的,后面2. 3. ...
等其余属性其实是应用到每一个缓存组的公共配置,同样的参数以缓存组的值为准。
把默认缓存组defaultVendors
或default
设置为 false,即可禁用对应缓存组规则。
模块必须符合某个缓存组的所有条件,才会被分割。
- 1.1
test
模块匹配规则,可以匹配模块资源绝对路径(函数或正则)或 chunk 名称(字符串),匹配 chunk 名称时(如'app'
),将选择 chunk 中的所有模块。
可选值:function (module, { chunkGraph, moduleGraph }) => boolean | RegExp | string
cacheGroups: {
chunks: 'all',
react: { // 1. 正则匹配示例,把 react 和 react-dom 分到一个名为 `lib-react` 的 js 中
// `[\\/]` 是作为跨平台兼容性的路径分隔符
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'lib-react',
},
svgIcon: { // 2. 函数匹配示例,把自定义 svg 图标都拆出来,放到 `svgIcon.js` 中
test(module) {
// `module.resource` 是文件的绝对路径
// 用`path.sep` 代替 / or \,以便跨平台兼容
const path = require('path'); // path 一般会在配置文件引入,此处只是说明 path 的来源,实际并不用加上
return ( // 匹配 icon 文件夹下的 .svg 后缀文件
module.resource &&
module.resource.endsWith('.svg') &&
module.resource.includes(`${path.sep}icons${path.sep}`)
);
},
name: 'svgIcon'
},
},
- 1.2
priority
:
默认值:-20
缓存组打包的优先级/权重,数值大的优先。
一个模块同时满足多个缓存组的条件,会优先考虑权重最高的那个缓存组。
默认组的优先级为负,以允许自定义组获得更高的优先级 (自定义组的默认值为0
)。 - 1.3
reuseExistingChunk
默认值:true
如果当前 chunk 包含已从主 chunk 中拆分出的模块,那么缓存组不会在新 chunk 内生成这个/些模块,而是去复用被拆出的 module。
这可能会影响 chunk 的结果文件名。
2. chunks
默认值:"async"
选择进行代码分割的 chunk 类型,可选值:"all"
| "async"
(异步) | "initial"
(同步)
默认配置只会对按需加载的代码进行分割。那么入口文件同步依赖的第三方包和公共模块是无法拆出来的。因此通常会将SplitChunk
整体的值设置为"all"
,把初始加载的代码也加入到分割的“受众”中来。在具体缓存组如有需要再按实际情况再覆盖。
如果设为"initial"
,那么该缓存组只会分离应用初始加载需要的包。有时这是有必要的,因为设为一味设为"all"
的话,打包出来的 js 都会在应用初始载入时加载,即使里面包含一些首页用不到的模块。
3. automaticNameDelimiter
默认值:'~'
此选项可以指定生成名称中的分隔符。
默认情况下,若未用cacheGroups.{cacheGroup}.name
自定义 chunk 名, webpack 会使用 chunk 的缓存组名和entry
来源生成 chunk 名(例如default~index.js
、defaultVendors~admin.js
)。
4. minSize
默认值:20000
生成 chunk 的最小体积 (以bytes
为单位)
新拆出的 chunk 的体积最小值,也就是符合缓存组其他条件的前提下,体积大于等于这个值的模块/模块集合才会被拆分出来。
比如我们有两个入口 chunk,各自都包含了一个模块m
(或者均有m1
和m2
),本来符合默认配置中的default
缓存组,但由于这个模块(或者m1
加上m2
)体积不足 20kb,便无法被输出为一个文件。
⚠️ 即使不匹配任何一个缓存组,在 splitChunks.* 的
minSize
选项会影响异步 chunk。规则是体积大于minSize
值的公共模块会被拆出。(除非 splitChunks.*chunks: 'initial'
,才没有这种影响)
公共模块即>= 2
个异步 chunk 共享的模块,同minChunks: 2
。
5. minChunks
默认值:1
拆分前必须共享模块的最小 chunks 数。
比如数值是2
,那么在符合某个缓存组其他规则的前提下,拆分前必须有 2 个 chunk 共用了这个模块,才可以被归到这个组下拆分出来。
不是文件共享而是 chunk 共享,所以清楚 SplitChunksPlugin 处理前 chunk 的分包情况非常有必要。
6. maxInitialRequests
默认值:30
每个入口点的最大并行请求数。
也就是每个入口和它的同步依赖最多够被拆分/合并成几个js
文件。对这个数量进行限制为的是避免初始js
请求过多。
注意几点:
- 入口文件本身算一个请求
- 如单独拆出了
runtimeChunk
,不算在内 - 单独拆出的
css
文件不算在内 - 若同时有两个模块满足
cacheGroup
规则要进行拆分,但maxInitialRequests
只允许再拆出一个文件,那么体积较大的模块会被拆分出来。
7. maxAsyncRequests
默认值:30
每个按需加载模块的最大并行请求数。
也就是一个异步加载模块和它的同步依赖最多能被拆分成几个js
。除了处理对象不同,应该很好理解。
注意几点:
-
import()
文件本身算一个请求 - 同样不算
js
以外的公共资源请求如css
- 若同时有两个模块满足
cacheGroup
规则要进行拆分,但maxAsyncRequests
只允许再拆出一个文件,那么体积较大的模块会被拆分出来。
8. enforceSizeThreshold
默认值:50000
如果符合缓存组其他条件(不包括下面三项)的模块/模块集超过这个体积阈值,就忽略minRemainingSize, maxAsyncRequests, maxInitialRequests
的配置,总是为这个缓存组创建 chunk。
也就是说即使超出了maxAsyncRequests
或maxInitialRequests
指定的可拆分次数,只要缓存组模块体积大于50kb
,仍然会分出新 chunk。
干货讲完,实战另开一篇一面:【webpack SplitChunksPlugin vue-cli 4 拆包实战】
参考文章:
webpack高级概念code splitting 和 splitChunks (系列五)
有点难的知识点: Webpack Chunk 分包规则详解