SplitChunksPlugin初见
自Webpack4以后,代码拆分的插件由CommonsChunkPlugin变成了SplitChunksPlugin,并且不必单独引用,集成到了Webpack之中,通过配置下的optimization.splitChunks和optimization.runtimeChunk就可以控制了。
研究插件经验
前两天研究了一下CommonsChunkPlugin插件,总结出来一条经验,就是要理解这个插件,单纯看如何配置它,是不会懂它的。
先知道它的设计思路,再学习如何配置它。
CommonsChunkPlugin的不足
Webpack4的代码拆分方案,完全换了一个插件,想必设计思路和使用上差别会比较大,实际上也的确如此。
如果是一些简单的重复代码的拆分,CommonsChunkPlugin是可以胜任的。但一些复杂的场景,CommonsChunkPlugin就不行了。
复杂场景举例
我们的项目结构是这样的:
- 有一个入口文件: index.js
- 三个用于异步加载的文件:Greeter1.js、Greeter2.js、Greeter3.js
- Greeter1.js、Greeter2.js引用了React.js,Greeter3.js 引用了Vue.js
使用CommonsChunkPlugin的情况
这时候用Webpack打包此项目,使用CommonsChunkPlugin的话,会将React.js Vue.js这些库打包到vendor.js中。
这样做的问题:
- 我们得到了一个非常大的公共Chunk。浏览器加载我们的项目首屏时,会加载入口Chunk index.js和公共Chunk vendor.js。这样对首屏加载速度是不利的。
- 某用户只是看Greeter1.js和Greeter2.js这两个Chunk的内容,那么对于他来说,加载Vue.js这样根本用不到的库,只是浪费流量。
理想的拆分和使用情况
不是将React.js、Vue.js打包到同一个vendor Chunk中,而是Webpack通过分析,将React.js打包到一个vendor~Greeter1~Greeter2.js中,将Vue.js打包到一个vendor~Greeter3.js中,这样分别打包公共代码。
然后首屏加载的时候,只加载入口Chunk index.js。等用户查看Greeter1.js的时候,再并行加载Chunk Greeter1.js和Chunk vendor~Greeter1~Greeter2.js。查看Greeter3.js的时候,再并行加载Chunk Greeter3.js和Chunk vendor~Greeter3.js。
这样,解决了上面提到的两个问题,首屏速度和流量浪费。
使用SplitChunksPlugin解决
SplitChunksPlugin就是可以应付上面描述的复杂的拆分情况,比较理想的拆分代码。
搭建实验项目
按照上面描述的,我们新建文件,目录结构如下:
文件内容如下:
// index.js
// 这样就是异步加载Greeter1、2、3 三个Chunk
import(/* webpackChunkName: "Greeter1" */'./Greeter1').then(module => {
const greeter = module.default
document.querySelector("#root").appendChild(greeter());
})
import(/* webpackChunkName: "Greeter2" */'./Greeter2').then(module => {
const greeter = module.default
document.querySelector("#root").appendChild(greeter());
})
import(/* webpackChunkName: "Greeter3" */'./Greeter3').then(module => {
const greeter = module.default
document.querySelector("#root").appendChild(greeter());
})
// Greeter1.js / Greeter2.js
// 我们就这样模拟引用了React。Greeter2.js和Greeter1.js长得一样。
import React from 'react';
console.log(React);
export const greeter = function () {
var greet = document.createElement('div');
greet.textContent = "Hi there and greetings!";
return greet;
};
// Greeter3.js
// Greeter3中引用的是Vue
import Vue from 'vue';
console.log(Vue);
export const greeter = function () {
var greet = document.createElement('div');
greet.textContent = "Hi there and greetings!";
return greet;
};
再来看看关键的Webpack配置
module.exports = {
entry: {
index: __dirname + "/app/index.js",
},
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "[name].js", //打包后输出文件的文件名
chunkFilename: '[name].js',
},
mode: 'development',
devtool: false,
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
},
}
}
},
}
关键的就是optimization下的配置,我们先只关注cacheGroups下的vendors配置。其中的test是分割代码的规则,代表node_modules文件夹下的代码都要被抽离出来。我们运行一下Webpack,看看输出结果:
我查看了一下vendor~Greeter1~Greeter2.js文件,里面是React打包后的代码。 我查看了一下vendor~Greeter3.js文件,里面是Vue库打包后的代码。
再利用打包分析,看到:
这不就是我们设想的那种理想情况。如果是CommonsChunkPlugin,配置后,只会帮我们打包出一个vendor.js的公共Chunk,而SplitChunksPlugin,我们只是告诉它node_modules下的文件要抽离出来,Webpack就根据项目的引用情况,自动分理处两个公共Chunk vendor~Greeter1~Greeter2.js 和 vendor~Greeter3.js
SplitChunksPlugin设计思路总结
解决复杂场景下的代码拆分问题。针对异步加载中公共模块的拆分,我们只需设置需要被公共打包的代码,SplitChunksPlugin就会自动帮我们按照各异步模块的需求,将公共的Chunk拆分成一些小的公共Chunks。供各异步模块使用。并且这些公共Chunks不会首屏加载,会随着使用使用它们的异步模块,使用时再一同并行加载。
核心思路:根据我们给的规则拆分代码,然后针对拆分的公共Chunk,再次拆分。
拆分出来的Chunk过多,怎么办
到这里,还需一种极端情况,就是被拆分出来的公共Chunk,太多了。Webpack的初衷是合并代码啊,这又给拆碎了。
过多Chunk导致的问题就是浏览器同时需要并发请求太多的js。
同样的SplitChunksPlugin也替我们想到了。
我们再来做一个实验。
复杂场景举例:
- 有一个入口文件: index.js
- 有四个用于异步加载的文件:Greeter1.js、Greeter2.js、Greeter3.js、Greeter4.js。
- 有三个给上四个文件引用的文件:helper1.js、helper2.js、helper3.js(注意这里helper*.js都要大于30k,太小了Webpack不会将其抽离出一个Chunk)。
- Greeter1.js引用helper1.js,Greeter2.js引用helper2.js,Greeter3.js引用helper3.js,,Greeter4.js同时引用helper1.js、helper2.js、helper3.js。
我画一个图说明情况:
G1代表Greeter1.js,h1代表helper.js。依此类推。这样的引用,形成了helper1~3都成了公共模块。需要我们将其提取出Chunk。使用Wepback编译,查看结果:是否跟你预想的一样,出现了三个公共Chunk,也就是我们上图画的公共部分,分别包含了header1~3的代码。再看更直观的效果图:
Greeter4.js的问题
浏览器加载Greeter4.js的时候,需要同时加载default~Greeter1~Greeter4.js、default~Greeter2~Greeter4.js、default~Greeter3~Greeter4.js三个Chunk。也就是用户看Greeter4.js时,需并行请求4个js文件。
问题就在于我们把公共包拆的过于细,有可能会出现,加载一个异步Chunk的时候,需要同时并且请求很多的公共Chunk,这不是我们想看到的,为此,SplitChunksPlugin提供给我们一个属性maxAsyncRequests,限制最大并行请求数。
目前的最大的并行请求数是加载Greeter4.js时的4,我们设置成3,看看什么效果:
optimization: {
splitChunks: {
maxAsyncRequests: 3, // 在此设置
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
运行webpack,效果如下:
Greeter4将helper3.js打到一个Chunk里,然后helper1、helper2单独打包,这样Greeter4的并行请求数等于3,符合预期。
同样的将helper3.js同样被Greeter3.js引用,所以也打包到了Greeter3中,造成了重复打包helper3。为了减少并且请求数,就会导致一定程度的重复打包,我们要做的,就是通过配置在平衡并且请求数和重复打包率上做一个平衡。
总的来说SplitChunksPlugin还是很智能啊,我们只是提出要求(并行请求数要小于等于3),它就会基于此条件为我们的进行拆包和组合包。
SplitChunksPlugin默认配置
即使我们不写optimization,Webpack也会帮助我们进行代码拆分,相当于我们写了如下的配置:
splitChunks: {
chunks: "async", // 默认只处理异步chunk的配置
minSize: 30000, // 如果模块的最小体积小于30,就不拆分它
minChunks: 1, // 模块的最小被引用次数
maxAsyncRequests: 5, // 异步加载Chunk时的最大并行请求数
maxInitialRequests: 3, // 入口Chunk的最大并行请求数
automaticNameDelimiter: '~', // 文件名的连接符
name: true, // 此处写成false,公共块就不会是default~Greeter1~Greeter4.js了,而是0.js这样命名Chunk。
cacheGroups: { // 缓存组,拆分Chunk的规则
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 此数越大,越优先匹配
},
default: {
minChunks: 2, // CommonsChunkPlugin的minChunks既可以传方法,也可以传数字,现在只可以传数字了,如果你想传方法,用test属性
priority: -20,
reuseExistingChunk: true // 配置项允许重用已经存在的代码块而不是创建一个新的代码块。这句我不懂,有知道的小伙伴麻烦告诉我一下
}
}
}
可以看到默认配置只对异步加载的Chunk有效,原因是配置了 chunks: "async"。
以下是默认配置的描述:
- 被共享的代码块或者来自node_modules文件夹
- 被分割的代码块要大于30kb(在min+giz之前)
- 按需加载代码块的请求数量应该<=5
- 页面初始化时加载代码块的请求数量应该<=3
这些描述分别对应了上面哪条配置,相信大家都清楚了。如果没有经过分析,这些描述真是让人摸不着头脑。
maxInitialRequests
maxInitialRequests字段我们还没有解释,看字段名字应该是初始化时,也就是针对入口Chunk的分割吧,于是我做了如下配置:
module.exports = {
entry: {
Greeter1: __dirname + "/app/Greeter1.js",
Greeter2: __dirname + "/app/Greeter2.js",
Greeter3: __dirname + "/app/Greeter3.js",
Greeter4: __dirname + "/app/Greeter4.js",
},
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "[name].js", //打包后输出文件的文件名
chunkFilename: '[name].js',
},
mode: 'development',
devtool: false,
optimization: {
splitChunks: {
chunks: "initial", // 默认只处理异步chunk的配置
maxInitialRequests: 3, // 一个入口最大并行请求数
cacheGroups: { // 缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
}
我们来看打包效果:
我将maxInitialRequests调成5,再来看打包效果:
打包的结果,和我们分析异步Chunk的提取策略一致,限制为5的时候,即使是Greeter4.js的最大并且请求数才是4,所以可以尽情的拆包。但限制为3的时候,Webpack就不把helper3.js单独拆成一个公共Chunk了,而是分别打包到引用了它的Greeter4.js和Greeter3.js里,以此来限制Greeter4这个入口Chunk被加载时,并行请求为3。可以说maxInitialRequests就是 针对多入口限制拆包数量的maxAsyncRequests。
拆分runtime的代码
说了这么多,还没有提到拆分runtime呢。SplitChunksPlugin拆分runtime只需配置一个属性,如下:
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: "initial", // 默认只处理异步chunk的配置
minSize: 30000, // 如果模块的最小体积小于30,就不拆分它
minChunks: 1, // 模块的最小被引用次数
maxAsyncRequests: 5, // 按需加载的最大并行请求数
maxInitialRequests: 5, // 一个入口最大并行请求数
automaticNameDelimiter: '~', // 文件名的连接符
name: true,
cacheGroups: { // 缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
我这里还是沿用上一个例子,打包结果如下:
很有意思,针对我们四个入口文件,分别生成了四个文件,runtime~Greeter1到4。这也符合预期,使用哪个入口的代码,就也加载它对应的runtime文件。
让我们回到最初
现在,我们还是回到最初的一个简单的例子,结束我们今天的研究。
基本场景
不考虑异步加载模块,只是分离业务代码,第三方库代码和runtime代码。
配置
入口文件index.js,里面只引用了react。 配置如下:
module.exports = {
entry: {
index: __dirname + "/app/index.js",
},
output: {
path: __dirname + "/public",//打包后的文件存放的地方
filename: "[name].js", //打包后输出文件的文件名
chunkFilename: '[name].js',
},
mode: 'development',
devtool: false,
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: "initial",
automaticNameDelimiter: '~',
name: true,
cacheGroups: { // 缓存组
vendors: {
test: /[\\/]node_modules[\\/]/,
},
}
}
},
}
打包结果
我们看到react是第三方库,提取到了vendors~index.js中,runtime代码,提取到了runtime-index.js,业务代码,就是index.js。
结束语
Webpack的官方文档,没有解释的那么清楚,对于Webpack的学习,需要多多动手,在实践中,帮助我们学习体会Webpack。
SplitChunksPlugin要理解起来还是稍微复杂一点的,它的设计就是为了搞定复杂的拆分情况。但摸清它的原理后,发现它还是很强大的,通过几项配置,就可以完成复杂情况下的代码拆分。