本文的目的在于简单的介绍webpack的优化功能配置:splitChunks。
webpack5出于“开箱即用”的目的,将大部分曾经要使用插件的功能集成到了config配置中,因此用户只需要了解如何配置,即可达到优化目的,其中最常使用接触的配置是:webpack.optimization.splitChunks。
一、拆分方法的比较
参考:
Webpack常用知识点
说到拆分,往往我们会有以下几个选择:
动态引入:import('')
、webpack.entry配置多个入口
、 splitChunks拆分抽取公共代码
相比较而言:
- 动态引入的拆分需要对一个个文件单独处理,不能整体规划,更适合写代码时考虑。
- 配置多个入口,大多时候没有此必要,而且容易将公共模块重复打包,拆分策略不一定好。
- splitChunks拆分:1. 配置在打包时,开发过程中可以不考虑。2. 可以整体考虑公共代码,比如react / utils等公共包。3. 使用webpack-analyze有利于更具体看到打包后的效果。
二、如何使用
webpack官方文档
详解利用webpack的splitChunk拆分打包文件
splitChunks中提供了一些字段,常用的有下列属性(其他属性大多时候不需改动):
chunks
分为all(推荐)|async(默认)|initial ,区别在于打包时模块的合并策略。
- all:推荐,不论是同步引入还是异步引入,都会被合并打包。
- async:和目前我们不设置任何配置的情况相同,同步引入的模块会被一起打包,而动态进入的模块是自动拆分到另一个包中。
- initial:同步引入的模块会被合并抽取成新的包,而异步引入的代码会被拆分成另一个包,比如a.js和b.js一个动态引入react,一个同步引入react,这时候打包出来的就是vendors-react(同步)、react(异步)两个react包而不会合并,这样就造成了浪费。
minSize
最小尺寸,默认是30K,development 下是10k,设置的越大满足该尺寸的chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中),当这个值很大的时候就不会做公共部分的抽取了。
minChunks
如果一个模块被多个chunk所引用的次数达到了minChunks指定的次数,那么这个模块就会被打包成一个单独的chunk。但是因为我们的项目只有一个入口文件,Webpack只会生成一个chunk,这时minChunks就没有作用了,因为打包结果只有一个chunk,不需要进行代码分离。
maxInitialRequests
参考:https://www.cnblogs.com/kwzm/p/10316217.html
maxInitialRequests是splitChunks里面比较难以理解的点之一,它表示允许【单个】入口并行加载的最大请求数,之所以有这个配置也是为了对拆分数量进行限制,不至于拆分出太多模块导致请求数量过多而得不偿失。
这里需要注意几点:
- 入口文件本身算一个请求
- 如果入口里面有动态加载得模块这个不算在内
- 通过runtimeChunk拆分出的runtime不算在内
- 只算js文件的请求,css不算在内
- 如果同时又两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来
cacheGroups
cacheGroups(缓存组)是 webpack splitChunks 最核心的配置,splitChunks的配置项都是作用于cacheGroup上的,也就是cacheGroups缓存组可以继承和覆盖来自 splitChunks.* 的任何选项。
接下来我们也是主要使用这个配置去拆分合并代码。
priority
拆分优先级,webpack首先会遍历依赖,生成依赖树,然后对单个模块打包,最后再拆分合并。这个属性可以决定一个模块同时属于多个合并规则的时候将合并进哪个文件内。
reuseExistingChunk
如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
test
当test为函数时,返回true/false,并且接收两个参数:module和chunks
module:每个模块打包的时候,都会执行test函数,并且传入模块 module 对象,module 对象包含了模块的基本信息,例如类型、路径、文件 hash 等;
chunks:是当前模块被分到哪些chunks使用,module 跟 chunks 关系可能是一对一或者多对一。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test(module, chunks) {
//...
return module.type === 'javascript/auto';
}
}
}
}
}
};
三、具体实现
修改完配置之后,我们可以通过webpack-analyze
看到打包出来的具体效果,衡量自己的优化程度。
在next中,就是如下配置:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withTM(withBundleAnalyzer(withImages(withAntdLess(nextConfig))));
npm run analyzer
一般而言,没有改动过的Next自带的配置如下:(从next.config中打印出来的数据)
{
emitOnErrors: true,
checkWasmTypes: false,
nodeEnv: false,
splitChunks: {
chunks: [Function: chunks],
cacheGroups: { {
framework: {
chunks: 'all',
name: 'framework',
test: [Function: test],
priority: 40,
enforce: true
},
lib: {
test: [Function: test],
name: [Function: name],
priority: 30,
minChunks: 1,
reuseExistingChunk: true
}
},
maxInitialRequests: 25,
minSize: 20000
},
runtimeChunk: { name: 'webpack' },
minimize: true,
minimizer: [ [Function (anonymous)], [Function (anonymous)] ]
}
想要增加分包策略,只需要像如下代码,在next暴露出的config中配置
webpack(config, { webpack, isServer }) {
try {
if (!isServer) {
const cacheGroups = config.optimization.splitChunks.cacheGroups;
console.log(cacheGroups)
}
} catch (e) {
console.error('webpack cacheGroups error: ',e);
}
return config;
}
我的主要目的在于优化项目的首包,也就是app.js,所以主要查看的也是这个包。
首先打印出项目的打包文件如图:(假装有图)
分析也许是受限于:maxInitialRequests,或者其他的原因,react / mobx等包都没有拆分出app的包,须知,这种固定依赖往往可以放在CDN中很久不变动,要是每次都跟着app包重新下载,不利于CDN利用和首屏渲染。
故第一份优化配置如下:
拆分公共依赖库:antd / mobx / react / moment / lodash等等,当然后续还可以拆分其他的模块。
const cacheGroups = config.optimization.splitChunks.cacheGroups;
// 1.mobx
cacheGroups.antd = {
name: 'antd',
test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
enforce: true,
chunks: 'all',
};
// 2.工具类
cacheGroups.vendors = {
name: 'vendors',
test: /[\\/]node_modules[\\/](mobx|axios|lodash|moment)[\\/]/,
enforce: true,
chunks: 'all',
priority: 20,
};
// 3.utils&config
cacheGroups.utils = {
name: 'utils',
test: /[\\/]src[\\/]utils[\\/]/,
enforce: true,
chunks: 'all',
priority: 20,
};
拆分后,可见打出来三个新的包antd 、vendors、utils,项目的整体大小少了0.6M,如果觉得antd的包过大,也可以再使用maxSize限制,或者细化test正则。
接下来再分析app的页面,看到某个模块下引入的配置文件基本上占用了一半的大小,这个配置文件是在一个大的activity文件夹下的多个小活动中的config.js。这是因为之前为了在服务器端渲染的时候能够获取到配置数据,所以在_app.js引入造成的问题,随着后续的需求展开,这里的影响可能会越来越大,因此必须得想个办法拆分出去。
想要把这一部分的包拆分出去,一开始考虑了几个方法:
- import动态引入分割。
- cacheGroup中添加代码分割的规则。(不能直接使用test是因为文件夹下有很多文件,一旦使用test会把所有文件都打包进来,不符合要求)
- 把这部分配置集中到另一个文件夹内,这样可以使用test去识别分割。
- 以上办法都不行的话,就把配置代码从_app中移出来,放到组件里面去动态处理生成。
方法一:
一开始写了一个类似下方这样的动态引入代码,但是在mobx数据初始化时才会进行下载,无法获取到配置数据。
await ConfigRes.then((Config) => {
const ConfigResModule = ConfigRes.default;
});
方法二:
经过尝试最后把test的名称正则改成只识别配置文件夹下的config.js文件。
优先级不能填太高,优先级填的高的话,activity下方所有活动的文件都会被打包进来,有使用和没有使用的文件都放在了一起,这样反而无法达到减负的目的。因此优先级填最低,这样可以使“未被划分进其他包、最后只能留在app.js包”中的这一些activity数据抽取出来。
再做一层保险设置maxSize,以免以后配置了太多活动导致这个包过大。
cacheGroups.activity = {
name: 'activity',
test: /[\\/]src[\\/]activity[\\/].*[\\/]config.js/,
enforce: true,
chunks: 'all',
priority: -30,
maxSize:200000,
};
方法三:
方法二其实比较繁琐,但是这样做可以使开发可以不去关注打包信息,只关注自己要开发的代码,而不用把代码挪过来挪过去。
方法四:
方法二已实现后,暂时没有考虑。
四、数据比对
需要做性能测试。
五、安全考虑
可能要经过压测和稳定性测试,以及性能测试看看是不是真的有效。