代码分割原理和配置
代码分割是利用现代前端打包构建工具的能力,将单个构建产物文件主动拆分为多个文件,从而提高缓存命中率、改善用户体验的优化。
以 Webpack@5 为例,代码分割的配置项主要有:
1.chunks
值类型: String | function (chunks) => string
功能:指定哪些类型的区块可以被纳入分割出的新区块。
chunks 有4种值:
'async' :分割出的新区块只允许包含动态加载的区块。
'initial' :分割出的新区块只允许包含非动态加载的区块。
'all' :分割出的新区块可以包含动态加载和非动态加载的区块。
函数 :配置一个函数,接收目标区块的数据作为参数,返回布尔 值,表示目标区块能否被纳入分割出的新区块,例如
- minSize
值类型: Number
功能:指定分割所产生的新区块的最小体积,单位为字节(byte),小于 minSize 字节的新区块,将不会被创建,也就不会产生对应的打包产物文件。
minSize 和 maxSize 衡量的都是组成区块的未压缩前的源码体积,不一定等于构建产物文件的体积。 - maxSize
值类型: Number
功能:指定新区块的最大体积,单位为字节(byte),大于 maxSize 字节的新区块,将被拆分为多个更小的新区块。 - minChunks
值类型: Number
功能:指定模块最少被多少个区块共同引用,才能被纳入分割出的新区块。 - maxInitialRequests
值类型: Number
功能:指定最多可以拆分为多少个同步加载的新区块,常用于和 maxAsyncRequests 配合,控制代码分割产生的最大文件数量 - maxAsyncRequests
值类型: Number
功能:指定最多可以分割出多少个异步加载(即动态加载 import() )的新区块,常用于和maxInitialRequests 配合控制代码分割产生的最大文件数量。 - name
值类型: String | Boolean | function (module, chunks, cacheGroupKey) => string
功能:指定分割出的区块名,区块名是Webpack运行时内部用来区分不同区块的id。
区块名不一定等于打包产物的文件名,当没有指定 cacheGroup.filename 时,区块名才会被用作产物文件名。
另外,对多个新区块或多个 cacheGroup[i] 配置相同的 name ,会使这些区块被合并,最终会被打包进同一产物文件中。
相反地,对多个区块配置不同的 name ,会使这些区块各自独立,最终多个独立的产物文件。 - cacheGroups
值类型: Object
功能:指定有独立配置的区块,既可以继承上述 splitChunks 的配置,也可以指定专属当前区块的独立配置。
可以理解为继承自 splitChunks 的子类,一方面继承了父类 splitChunks 分割区块的能力和配置属性,另一方面也有自己的私有属性。 - test
值类型: Regex | String | function (module, { chunkGraph,moduleGraph }) => boolean
指定当前缓存组 cacheGroup 区块包含模块的匹配规则。
其值有3种类型:
(1)正则表达式:用于对模块文件的绝对路径,调用 regExp.test(modulePath) 方法,判断当前缓存组区块是否包含目标模块文件。
(2)函数:接收 (module, { chunkGraph, moduleGraph }) 作为参数,返回布尔值表示当前缓存组区块是否包含目标模块文件。
(3)字符串:用于对模块文件的绝对路径,调用 modulePath.startsWith(str) 方法,判断当前缓存组区块是否包含目标模块文件。 - priority
值类型: Number
指定当前缓存组区块的优先级,当一个模块文件满足多个缓存组区块的匹配规则( .test 属性)时,最终会将模块文件分割进 priority 值更大的那个缓存组区块。 - filename
值类型: String | Boolean | function (pathData, assetInfo) => string
指定区块对应打包产物文件的文件名,支持:(1)使用 [contenthash] 等文件名替换符。(2)指定文件类型,即文件的后缀名。 - enforce
值类型: Boolean
指定是否忽略 maxSize, minSize, maxAsyncRequests, maxInitialRequests 等配置项的限制,强制生成当前缓存组对应的区块。
传统代码分割的痛点
1.配置复杂,开发体验不佳:各类繁杂的配置项令开发者困惑,难以确定拆分目标模块。
2.配置方案健壮性不强,可维护性不好:拆分配置方案无法适应项目的快速迭代变化,需要经常调整;
3.用户体验不好:拆分效果不好,拆分出的模块每次打包上线都会变化,不便于配合增量构建进行缓存,没有实现最优缓存效果,甚至使用户体验恶化;
如何解决这些痛点?代码分割的最佳实践是什么?
细粒度代码分割(Granular Code Split) 是近年来发明的代码分割通用解决方案,其经过Next.js, Gastby 等前端SSR框架多年的实践验证,能有效解决上述痛点,显著改善开发体验和用户体验。
其核心思路是通过拆分出更多的区块、更多产物文件,让每个产物文件拥有自己的哈希版本号文件名,对产物文件的缓存有效性做细粒度的控制,让前端项目在多次打包上线后,仍然能复用之前的产物文件,不必重新下载静态资源。
-
核心配置
细粒度代码分割的核心Webpack配置如下:
// webpack.production.config.js
const crypto = require('crypto');
const MAX_REQUEST_NUM = 20;
// 指定一个 module 可以被拆分为独立 区块(chunk) 的最小源码体积(单位:byte)
const MIN_LIB_CHUNK_SIZE = 10 * 1000;
const isModuleCSS = (module) => {
return (/*...*/)
};
module.exports = {
mode: 'production',
optimization: {
splitChunks: {
maxInitialRequests: MAX_REQUEST_NUM,
maxAsyncRequests: MAX_REQUEST_NUM,
minSize: MIN_LIB_CHUNK_SIZE,
cacheGroups: {
defaultVendors: false,
default: false,
lib: {
chunks: 'all',
test(module){
return(
module.size()>MIN_LIB_CHUNK_SIZE&&
/node_modules[/\\]/.test(module.identifier())
);
},
name(module){
return'lib_'+ hash.digest('hex').substring(0,8)
},
priority:3,
minChunks:1,
reuseExistingChunk:true,
},
shared:{
chunks:'all',
name(module,chunks){
return`shared.${crypto.createHash('sha1').update(
chunks.reduce((acc,chunk)=>{
return acc+chunk.name;
},''),
)
.digest('hex')
.substring(0,8)}${isModuleCSS(module)?'.CSS':''}`;
},
priority:1,
minChunks:2,
reuseExistingChunk:true,
},
},
},
},
};
这份代码分割配置分割出了2类区块:
1.lib :主要匹配规则为 test(module) ,指定 lib 缓存组包含来自 node_modules 目录,源码体积大于 MIN_LIB_CHUNK_SIZE 的模块。
lib 缓存组用于把体积较大的NPM包模块,拆分为独立区块,产生独立产物文件,从而在多次打包发版、更新哈希版本号文件名的同时,避免让用户再次下载这些大体积模块,提高缓存命中率,减少资源下载体积,改善用户体验。
2.shared : 主要匹配规则为 minChunks: 2 ,指定 shared 缓存组包含被2个及以上区块共用的模块代码。
2.优点
1.开发体验好:配置统一通用,自动选择拆分目标模块,不必人工判断哪些模块需要拆分,降低了代码分割的使用门槛;
2.健壮性强:以不变应万变,用这套不变的代码分割配置可以应对不断更新迭代的各类型前端项目,不必经常更新配置,便于维护;
3.用户体验好:分割颗粒度较细,产物文件稳定,多次构建部署后,仍有较多文件名称内容不变,缓存命中率高,缓存效果好,有利于改善用户体验。
为前端项目增加细粒度代码分割
1.确认优化前状态
优化开始前,我们先通过 webpack-bundle-analyzer 插件,确认前端工程项目当前的编译打包产物的状况。
- 增加细粒度代码分割缓存组
具体做法是,在Webpack配置中增加 lib , shared 2个缓存组( cacheGroup),将 vendors 这样体积较大的模块进一步合理、精细地分割。
具体的改动主要有3步:
(1) 删除原有的 vendors 缓存组:释放其包含的模块。
(2) 禁用默认缓存组:避免干扰细粒度代码分割。
(3) 新增 lib , shared 2个缓存组配置 - 确认分割效果
完成上述配置后,即可再次编译打包,通过 bundle-analyzer-plugin 确认分割后的模块。 - 获取全部产物清单
细粒度代码分割后的产物数量较多,如果需要完整清单,可以使用 webpack-manifest-plugin 随编译打包生成一份JSON格式的产物清单文件,用于服务端渲染时加载、生产环境部署。