前言
Webpack 默认会将尽可能多的模块代码打包在一起,优点是能减少最终页面的 HTTP 请求数,但缺点也很明显:
1 页面初始代码包过大,影响首屏渲染性能;2 无法有效应用浏览器缓存,特别对于 NPM 包这类变动较少的代码,业务代码哪怕改了一行都会导致 NPM 包缓存失效。
理解chunk
chunks代码块 assets资源 file文件 区别
- modules: 模块,每个文件就是一个模块
- chunks: 打包的每个文件属于独立的模块,然后webpack通过入口开始寻找依赖图,每个入口文件及其依赖的模块就是一个chunks
- assets: 资源,chunks打包后输出资源,内容就是字符串。
- file: 打包后的资源会写入硬盘,生成main.js文件
chunk详细
Chunk 是 Webpack 内部一个非常重要的底层设计,用于组织、管理、优化最终产物,在构建流程进入生成(Seal)阶段后:
1 Webpack 首先根据 entry 配置创建若干 Chunk 对象;2 遍历构建(Make)阶段找到的所有 Module 对象,同一 Entry 下的模块分配到 Entry 对应的 Chunk 中;(同步chunk)3 遇到异步模块则创建新的 Chunk 对象,并将异步模块放入该 Chunk;(异步chunk)4 分配完毕后,根据 SplitChunksPlugin 的启发式算法进一步对这些 Chunk 执行裁剪、拆分、合并、代码调优,最终调整成运行性能(可能)更优的形态;5 最后,将这些 Chunk 一个个输出成最终的产物(Asset)文件,编译工作到此结束。
Chunk 分包结果的好坏直接影响了最终应用性能,Webpack 默认会将以下三种模块做分包处理:
1 Initial Chunk:entry 模块及相应子模块打包成 Initial Chunk;2 Async Chunk:通过 import('./xx') 等语句导入的异步模块及相应子模块组成的 Async Chunk;3 Runtime Chunk:运行时代码(比如webpack的requre等代码,让其可以在浏览器环境运行)抽离成 Runtime Chunk,可通过 entry.runtime 配置项实现
其中,RunTimeChunk规则比较简单,而Initial Chunk和Async Chunk这种略显粗暴的规则则会带来两个明显问题。
- 1 模版重复打包。假如多个 Chunk 同时依赖同一个 Module,那么这个 Module 会被不受限制地重复打包进这些 Chunk
- 2 资源冗余和低效缓存。Webpack 会将 Entry 模块、异步模块所有代码都打进同一个单独的包
- 资源冗余:客户端每次都需要请求整个项目代码才能启动正常,但是用户可能只需要访问当下很小一部分内容。
- 缓存失效:所有的资源打成一个包,所有改动即使是修改了一个字符串,也会导致hash名称改变,从而导致浏览器缓存失效,客户端需要重新下载代码包。
而这两个问题都可以通过科学的分包策略解决,如
- 1 将多个Chunk依赖的包分离成独立的chunk,防止资源重复。
- 2 node_modules的资源变动较少,可以抽离成一个独立的包,业务代码的频繁改动不会导致第三方库资源缓存失效。
SplitChunksPlugin简介
splitChunksPlugin可以基于一些更灵活、合理的启发式规则将 Module 编排进不同的 Chunk,最终构建出性能更佳,缓存更友好的应用产物
splitChunksPlugin的主要能力有:
SplitChunksPlugin
支持根据 Module 路径、Module 被引用次数、Chunk 大小、Chunk 请求数等决定是否对 Chunk 做进一步拆解,这些决策都可以通过optimization.splitChunks
相应配置项调整定制,基于这些能力可以实现:- 单独打包某些特定路径的内容,例如
node_modules
打包为vendors
; - 单独打包使用频率较高的文件;
- 单独打包某些特定路径的内容,例如
SplitChunksPlugin
还提供了optimization.splitChunks.cacheGroup
概念,用于对不同特点的资源做分组处理,并为这些分组设置更有针对性的分包规则;SplitChunksPlugin
还内置了default
与defaultVendors
两个cacheGroup
,提供一些开箱即用的分包特性:node_modules
资源会命中defaultVendors
规则,并被单独打包;- 只有包体超过 20kb 的 Chunk 才会被单独打包;
- 加载 Async Chunk 所需请求数不得超过 30;
- 加载 Initial Chunk 所需请求数不得超过 30。
如下:
optimization: {splitChunks: {// 代码分为两类,第一类是初始化模块,第二个是异步模块。chunks: "all", // initial async all ,默认是aync, asyncb表示splitChunks只对异步的chunks生效,比如import('./)这些,initail表示只对同步chunks生效,比如根据入口和其依赖打包的chunks,all表示都生效。minSize: 30000, //默认值是30kb,分割的代码块最小尺寸。minChunks: 2, //在分割之前被引用的次数要大于2.maxAsyncRequests: 5, // 按需加载的时候的最大并行请求数小于5,一个文件中有多个import(),访问的时候一次性请求5个异步代码最多,剩余的import在下一次才请求maxInitialRequests: 3, //一个入口的最大并行请求数量(首页),就是打包后的代码,首页文件中需要加载的script脚本,bundle.js,打包后的第三方模块算一个(vendor.js),以及公用的模块也算一个(common.js)。比如首页的js文件所在的chunkjs,node_modules打包出来的,以及自己编写的公共模块。一共三个。如设置2,只能分割自己,以及node_Module中的模块,对于我们自己写的公共模块无法打包(common.js无法形成,因为不满足一个入口最大并行请求数量)。而如果chunkjs依赖了verdor.js和公共模块commonjs,那么打包后的代码就有一个循环依赖,只有commonjs 和verdor.js加载好了之后,调用改造好的Push方法,就会触发检查依赖的方法,然后只有判断verdor和common都加载完,才会执行main.js。//name: true, //打包后的名字,默认是chunk名字通过分隔符分开连接在一起。automaticNameDelimiter: "~", //名字分隔符。// 配置分割的文件cacheGroups: {vendors: {chunks: "initial", //分割的是同步的代码块。test: /node_modules/, //属于node_modules的模块,加载同步的并且是同步的就会被分割。priority: -10, // 优先级},commons: {chunks: "initial", //分割的是同步的代码块。minSize: 0,minChunks: 2, //最少被两个引用。priority: -10, // 优先级reuseExistingChunk: true, //如果该chunk引用了已经抽取的chunk,则会被打包。},},},
spligChunks主要有两种类型配置:
- minChunks/minSize/maxInitialRequest等分包条件,满足这些条件的模块都会被执行分包。
- cacheGroup:用于为特定资源声明特定分包条件,比如可以为nodeModules包设置更宽松的分包条件。
设置分包范围
optimization: {splitChunks: {// 代码分为两类,第一类是初始化模块,第二个是异步模块。chunks: "all", // initial async all ,默认是aync, asyncb表示splitChunks只对异步的chunks生效,比如import('./)这些,initail表示只对同步chunks生效,比如根据入口和其依赖打包的chunks,all表示都生效。}
根据module使用频率分包
SplitChunksPlugin
支持按 Module 被 Chunk 引用的次数决定是否分包,通过设置splitChunks.minChunks
optimization: {splitChunks: {minChunks: 2 //设定引用次数超过2的模块进行分包}
}
这里被chunks引用次数不直接等价于被import的次数,而是取决于上游调用者是否被视为同步chunk或者异步chunk处理(可以理解成被多少个chunks引用),如:
// common.js
export default "common chunk";// async-module.js
import common from './common'// a.js
import common from './common'
import('./async-module')// b.js
import common from './common'
a和b分别被视为initial chunk处理,async-modale被a以异步的方式引入,因此视为async chunk处理。
对于common来说。分别被三个不同的chunk引入,所以引用次数为3。
如果此时的配置为
module.exports = {entry: {entry1: './src/a.js',entry2: './src/b.js'},// ...optimization: {splitChunks: { minChunks: 2,//...}}
};
那么common模块会命中optimization.splitChunks.minChunks=2的规则,因此该模块可能被单独分饱,产物:
- a.js
- b.js
- async-module.js
- common.js
这里这是可能,1因为minChunks并不是唯一的条件,此外还需要满足如minSize,maxInitialRequest等配置的要求才会真正执行分包。
注意这里的引用次数算法
如果这里
a->common.js
b->common.js
如果a和b是单独的entry,即表示a和b会被设为不同的chunks,那么common的引用次数就是2。
而如果a引用了b,那么a和b就属于同一个chunk,那么common的引用次数就被设为1,并不是2。
限制分包数量
在minChunks的基础上,为了防止最终产物文件过多导致http网络请求数量剧增,反而降低应用性能,wbepack提供了maxInitialRequessts/maxAsyncRequests配置项,用于限制分包数量。
- maxInitialRequests:用于设置initial Chunk最大并行请求数。
- maxAsyncRequests: 用于设置Async Chunk最大并行请求数。
要注意这里所谓的请求数,不是说首次加载需要请求的http数量,而是加载一个chunks时所需要加载的分包数量。
以上述案例为例子:
- 对于a.js,加载这个chunks的时候,不仅要加载a.js,还需要加载common.js,那么commonjs对于a来说就是分包,所以此时chunkA的并行请求数就是自身+分包数=1+1=2。这里的async-module并不是initail Chunk,故不算进initial Chunk并行请求数。
再看一个demo
设minChunks=2,maxInitialRequests=2
a.js->common.jsb.js->common1.jsc.js->common.js+common1.js
如上,a,b,c被视为三个initital chunkm,common1和common都满足minChunks的要求,但是在加载c的chunk的时候,他的分包为common和common1,此时并行数量=1+2=3,并不满足maxInitailRequests=2的需求,此时,SplitChunksPlugin
会 放弃 common-1
、common-2
中体积较小的分包 ,也就是将体积大的包拆分出来,体积小的文件不拆分。maxAsyncRequest
逻辑与此类似
所以并行请求数量的关键逻辑总结如下:
- Initial Chunk 本身算一个请求;
- async chunk不算并行请求
- 通过runtimeChunk拆分出来的runTime不算并行请求。
- 如果同时有两个chunk满足拆分规则,但是maxInitailrequests只能允许再拆分一个模块,那么体积更大的模块会优先被拆解。
限制分包体积
除了minChunks-模块被引用次数以及maxXXXRequests-包数量,webpack还提供了与chunk大小有关的分包判定规则,包体积过小的时候直接取消分包,包体积过大的时候尝试对chunk再做拆解,防止单个Chunk过大。
相关的配置项:
minSize
: 超过这个尺寸的 Chunk 才会正式被分包;maxSize
: 超过这个尺寸的 Chunk 会尝试进一步拆分出更小的 Chunk;maxAsyncSize
: 与maxSize
功能类似,但只对异步引入的模块生效;maxInitialSize
: 与maxSize
类似,但只对entry
配置的入口模块生效;enforceSizeThreshold
: 超过这个尺寸的 Chunk 会被强制分包,忽略上述其它 Size 限制。
结合前面的两种规则,splitChunksPlugin的主体流程如:
- 1 尝试命中minChunks的module统一抽到一个额外的chunk对象。
- 2 判断该cunk是否满足maxXXXRequests阈值,若满足则进行下一步判断。
- 3 判断该chunk的资源体积是否大于上述配置项minSize声明的阈值下限,有几种情况:
- 如果该chunk的体积小于minSize,则取消这次分包。对应的module依然会合并到原来的chunk
- 如果chunk体积大于minSize,判断是否超过maxSize,maxAsyncSize/maxInitailSize声明的阈值上限,如果超过,会尝试将该chunk分割成更小的部分。
- 如果chunk的体积命中enforeSizeThreshold,那么会跳过上述的条件判断,直接进行分包。
缓存组 cacheGroups
上述的minChunks,maxInitailRequest,minSize都属于分包条件,决定什么情况下对那些满足条件的module做分包处理,此外,cacheGroups配置项用于为不同的文件组设置不同的规则。满足的module会优先按cacheGroup设置的条件进行分包。如
module.exports = {//...optimization: {splitChunks: {cacheGroups: {vendors: {test: /[\\/]node_modules[\\/]/,minChunks: 1,minSize: 0}},},},
};
上述设置了vendors缓存组,满足test正则匹配的模块都会被归为vendors分组,优先使用该分组下的minCunks,minSize等分包配置。
cacheGroups支持上述,minSize/minChunks/maxInitailRequest等条件配置,此外还支持*
test
:接受正则表达式、函数及字符串,所有符合test
判断的 Module 或 Chunk 都会被分到该组;type
:接受正则表达式、函数及字符串,与test
类似均用于筛选分组命中的模块,区别是它判断的依据是文件类型而不是文件名,例如type = 'json'
会命中所有 JSON 文件;idHint
:字符串型,用于设置 Chunk ID,它还会被追加到最终产物文件名中,例如idHint = 'vendors'
时,输出产物文件名形如vendors-xxx-xxx.js
;priority
:数字型,用于设置该分组的优先级,若模块命中多个缓存组,则优先被分到priority
更大的组。- …
缓存组的作用在于能为不同类型的资源设置更具适用性的分包规则,一个典型的场景就是将所有nodue_modules下的模块统一打包到vendors产物,从而实现第三方库与业务代码的分离,cacheGroups的默认配置是:
module.exports = {//...optimization: {splitChunks: {cacheGroups: {default: {idHint: "",reuseExistingChunk: true,minChunks: 2,priority: -20},defaultVendors: {idHint: "vendors",reuseExistingChunk: true,test: /[\\/]node_modules[\\/]/i,priority: -10,enforce: true//不受splitChunks.minSize等影响,强制打包}},},},
};
这两个配置组可以帮助我们:
- 将所有的mode_modules中的资源单独打包到venfors-xx-xx.js中
- 对引用次数大于等于 2 的模块 —— 也就是被多个 Chunk 引用的模块,单独打包(也受限splitChunks.minSize,maxInitialRequests影响,不一定会打包。没设置enforce)
配置总结
minChunks
:用于设置引用阈值,被引用次数超过该阈值的 Module 才会进行分包处理;maxInitialRequest/maxAsyncRequests
:用于限制 Initial Chunk(或 Async Chunk) 最大并行请求数,本质上是在限制最终产生的分包数量;minSize
: 超过这个尺寸的 Chunk 才会正式被分包;maxSize
: 超过这个尺寸的 Chunk 会尝试继续做分包;maxAsyncSize
: 与maxSize
功能类似,但只对异步引入的模块生效;maxInitialSize
: 与maxSize
类似,但只对entry
配置的入口模块生效;enforceSizeThreshold
: 超过这个尺寸的 Chunk 会被强制分包,忽略上述其它 size 限制;cacheGroups
:用于设置缓存组规则,为不同类型的资源设置更有针对性的分包策略。
最佳实践
- 针对nodeModules:
- 可以将node_mdodules模块打包成单独文件,通过cacheGroup实现,防止业务带啊吗的变更影响npm包缓存,同时通过maxSize设定阈值,防止vendor包体积过大。
- 更激进的,如果生产环境已经部署 HTTP2/3 一类高性能网络协议,甚至可以考虑将每一个 NPM 包都打包成单独文件。
- 针对业务代码:
- 设置common分组,通过minChunks配置项,将使用率较高的资源合并为common资源。
- 首评用不上的代码,比如除了主页外其他的页面,尽量使用异步导入。
- 设置optimization.runTimeChunk为true,将运行时代码拆分为独立资源。
总结
- Chunk 是 Webpack 实现模块打包的关键设计,Webpack 会首先为 Entry 模块(initail chunk)、异步模块(import(‘/’))、Runtime 模块(取决于配置) 创建 Chunk 容器,之后按照
splitChunks
配置进一步优化、裁剪分包内容。 - splitChunks的规则大致可以分为:
- 规则类,如统一的minSize, minChunks等
- cacheGroup: 针对特定资源的规则集合。被命中的模块会优先使用cacheGroup的规则进行分包。
学习文章:掘金:《Webpack5 核心原理与应用实践》