Code Splitting
总体来说,webpack提供了三种方法来实现代码拆分:
- 入口配置:多个
entry
入口起点; - 抽取公有代码:使用 SplitChunks 抽取公有代码;
- 动态加载 :动态加载一些代码。
抽取公有代码是难点,也是平时项目中经常会用到的,在这里只讨论使用 SplitChunks
抽取公有代码。
SplitChunks
SplitChunks
是由 webpack 4
内置的 SplitChunksPlugin
插件提供的能力,可直接在 optimization
选项中配置。
SplitChunks
抽取公有代码,默认情况下,SplitChunks
只影响按需加载的代码块(chunk),为什么这样呢,因为代码拆分会影响页面<script>
的数量及加载顺序等,如果使用html-webpack-plugin
之类的插件能很好自动地处理好页面<script>
标签的问题的话,我们可以通过chunks
属性来改变其只影响按需加载的行为。
webpack会根据满足以下条件来自动进行代码块分割:
- 新代码块可以被共享引用或这些模块都是来自
node_modules
文件夹里面 - 新代码块大于30kb(min+gziped之前的体积)
- 按需加载的代码块,最大数量应该小于或者等于5
- 初始加载的代码块,最大数量应该小于或等于3
例子一
// entry.js
import("./a");
// a.js
import "react-dom";
// ...
结果:webpack
会创建一个包含react-dom
的分离代码块。当import
调用时候,这个代码块就会与./a
代码被并行加载。
为什么会这样打包:
- 条件1:这个代码块是从
node_modules
来的 - 条件2:
react-dom
大于30kb - 条件3:按需请求的数量是1(小于5)
- 条件4:不会影响初始代码请求数量
这样打包有什么好处呢?
对比起你的应用代码,react-dom
可能不会经常变动。通过将它分割至另外一个代码块,这个代码块可以被独立缓存起来。
例子二:
// entry.js
import("./a");
import("./b");
// a.js
import "./helpers"; // helpers is 40kb in size
// ...
// b.js
import "./helpers";
import "./more-helpers"; // more-helpers is also 40kb in size
// ...
结果:webpack
会创建一个包含./helpers
的独立代码块,其他模块会依赖于它。在import
被调用时候,这个代码块会跟原始的代码并行加载。
为什么会这样打包:
- 条件1:这个代码块会被两个动态导入(import)调用依赖(指的是a.js和b.js)
- 条件2:helpers体积大于30kb
- 条件3:按需请求的数量是2(小于5)
- 条件4:不会影响初始代码请求数量
- 这样打包有什么好处呢?
将helpers代码放在每一个依赖的块里,可能就意味着,用户重复会下载它两次。通过用一个独立的代码块分割,它只需要被下载一次。实际上,这只是一种折衷方案,因为我们为此需要付出额外的一次请求的代价。这就是为什么默认webpack将最小代码块分割体积设置成30kb(译注:太小体积的代码块被分割,可能还会因为额外的请求,拖慢加载性能)。
通过optimizations.splitChunks.chunks: "all"
,上面的策略也可以应用到初始代码块上(inital chunks)。代码块也会被多个入口共享&按需加载。
如果想要自己控制拆分的功能,webpack提供很多选项来满足你的需求
配置
SplitChunks
其默认配置如下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
其选项说明如下:
chunks
:表示从哪些chunks
里面抽取代码,除了三个可选字符串值initial
、async
、all
之外,还可以通过函数来过滤所需的chunks
;minSize
:表示抽取出来的文件在压缩前的最小大小,默认为 30000;maxSize
:表示抽取出来的文件在压缩前的最大大小,默认为 0,表示不限制最大大小;minChunks
:表示被引用次数,默认为1;maxAsyncRequests
:最大的按需(异步)加载次数,默认为 5;maxInitialRequests
:最大的初始化加载次数,默认为 3;automaticNameDelimiter
:抽取出来的文件的自动生成名字的分割符,默认为 ~;name
:抽取出来文件的名字,默认为 true,表示自动生成文件名;cacheGroups
: 缓存组。
cacheGroups
上面那些 SplitChunks
的参数,其实都可以不用管,cacheGroups
才是我们配置的关键。它可以继承/覆盖上面 splitChunks
中所有的参数值,除此之外还额外提供了三个配置,分别为:test
, priority
和 reuseExistingChunk
默认情况是会将所有来自node_modules
的模块分配到一个叫vendors
的缓存组;所有重复引用至少两次的代码,会被分配到default
的缓存组。
可以通过optimization.splitChunks.cacheGroups.default: false
禁用default
缓存组。
default
缓存组的优先级(priotity
)是负数,因此所有自定义缓存组都可以有比它更高优先级(更高优先级的缓存组可以优先打包所选择的模块)(默认自定义缓存组优先级为0)
test
: 表示要过滤出的模块,默认为所有的模块,可匹配模块路径或chunk
名字,当匹配的是chunk
名字的时候,其里面的所有模块都会选中;priority
:表示抽取权重,数字越大表示优先级越高。因为一个模块可能会满足多个cacheGroups
的条件,那么抽取到哪个就由权重最高的说了算;reuseExistingChunk
:表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
当然,某一模块可能有被分配到多个缓存组的情况,这时,优化策略会将此模块分配到高优先级别(priority)的缓存组,或者会分配至可以形成更大体积代码块的组里。
配置实战
一般来说我们常用的配置都是 common + page 的形式。而 page 在 entry 入口的时候就已经配置好了。那么现在就只剩下 common 的处理。
cacheGroups: {
common: {
test: /[\\/]node_modules[\\/]/,
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2,
},
}
上面意思是:把所有从node_modules
引入超过 1 次的模块抽取到一个名为common
的代码块(chunk)
cacheGroups: {
common: {
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2,
},
}
上面的意思是,把所有引入超过 1 次的模块抽取为 common
代码块。
cacheGroups: {
reactBase: {
name: 'reactBase',
test: (module) => {
return /react|redux|prop-types/.test(module.context);
},
chunks: 'initial',
priority: 10,
},
common: {
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2,
},
}
上面的意思是,把所有匹配test
且引入超过 1 次的模块抽取为 reactBase
代码块。
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
}
}
}
把整个应用所有来自node_modules的代码。抽取为 vendors
代码块。
CSS 配置
同样对于通过 MiniCssExtractPlugin
生成的 CSS 文件也可以通过 SplitChunks
来进行抽取公有样式等。
如下表示将所有 CSS 文件打包为一个(注意将权重设置为最高,不然可能其他的 cacheGroups 会提前打包一部分样式文件):
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
priority: 20,
}
}
}
}
}