当前位置: 首页 > 科技观察

Webpack性能系列之四:分包优化

时间:2023-03-16 10:54:46 科技观察

1.什么是分包?数据包大小的逐渐增加可能会导致应用程序响应时间越来越长。归根结底,这种将所有资源打包到一个文件中的方法有两个缺点:“资源冗余”:客户端必须等待整个应用程序的代码包加载完毕才能开始运行,但是当前访问的内容用户可能只需要使用其中之一。部分代码“缓存失效”:所有资源整合成一个包后,全部发生变化——即使只修改一个字符,客户端也需要重新下载整个代码包,缓存命中率极低。比如node_modules中的资源,通常变化较小,可以抽离成一个独立的包,所以业务代码的频繁改动,不会导致这些第三方库资源被无意义的重复加载。为此,Webpack专门提供了用于产品分包的SplitChunksPlugin插件。2、使用SplitChunksPluginSplitChunksPlugin是Webpack4(原CommonsChunkPlugin)之后引入的分包方案,可以根据一些启发式规则将Module排列成不同的Chunk序列,最终将应用代码打包到多个产品中,从而实现分包功能。在使用上,SplitChunksPlugin的配置规则比较抽象,算是Webpack的一个难点。经过仔细拆解,关键逻辑是:SplitChunksPlugin通过模块引用频率、chunk大小、请求包数三个维度来决定是否进行分包操作。这些决策都可以通过optimization.splitChunks配置项进行调整和定制。基于这些维度,我们可以实现:将某些特定路径的内容单独打包,如node_modules打包为vendors将常用文件单独打包SplitChunksPlugin也提供了配置组概念optimization.splitChunks。cacheGroup,用于为不同类型的资源设置更有针对性的配置信息。SplitChunksPlugin也内置了两个配置组default和defaultVendors,提供了一些开箱即用的特性:node_modules资源会打defaultVendors规则,只会单独打包,只有包体超过20kb的chunk才会打包分别地。加载一个AsyncChunk需要的请求数不能超过30,加载一个InitialChunk需要的请求数不能超过30,这里说的请求数不能等同于http资源请求数,会下面讨论综上所述,分包逻辑基本围绕Module和Chunk展开。在介绍具体用法之前,有必要先温习一下Chunk的基础知识。2.1什么是Chunk在《有点难的知识点:Webpack Chunk 分包规则详解》一文中,我们了解到Chunk是包装产品的基本组织单元。读者可以等价地认为有多少个对应的产品(Bundle)就有多少个Chunk。webpack包含三种Chunk:InitialChunk:根据Entry配置项生成的ChunkAsyncChunk:异步模块引用,比如import(xxx)语句对应的异步ChunkRuntimeChunk:只包含运行时代码的ChunkAboutruntimeFor概念请参考《Webpack 原理系列六:彻底理解 Webpack 运行时》,SplitChunksPlugin默认只对AsyncChunk有效。开发者也可以通过optimization.splitChunks.chunks调整作用范围。该配置项支持以下值:String'all':对InitialChunk和AsyncChunk都有效,建议优先使用该值String'initial':仅对InitialChunk有效String'async':仅对AsyncChunk有效Function(chunk)=>boolean:函数返回true时有效例如:module.exports={//...optimization:{splitChunks:{chunks:'all',},},}2.2分包策略详解2.2.1SplitChunksPlugin支持根据Module被Chunk引用的次数进行分包,开发者可以通过optimization.splitChunks.minChunks设置最小引用次数,例如:module.exports={//...optimization:{splitChunks:{//设置子包引用超过4次的模块minChunks:3},},}需要注意的是,这里的“numberofreferencesbyChunk”并不直接等同于import的数量,而是取决于是否上游调用者被视为InitialChunk或AsyncChunk,例如://common.jsexportdefault"commonchunk";//async-module.jsimportcommonfrom'./common'//entry-a.jsimportcommonfrom'./common'import('./async-module')//entry-b.jsimportcommonfrom'./common'//webpack.config.jsmodule.exports={entry:{entry1:'./src/entry-a.js',entry2:'./src/entry-b.js'},//...优化:{splitChunks:{minChunks:2}}};上面的例子包含四个模块,形成如下模块关系图:例子中entry-a,entry-b分别作为InitialChunk;async-module是entry-a异步引入的,所以被当做AsyncChunk。对于公共模块,分别由三个不同的Chunk引入。当引用数为3时,命中了optimization.splitChunks.minChunks=2规则,所以这个模块“可能”被单独分包,最终产品:entry-a.jsentry-b.jsasync-module.jscommon.js2。2.2限制分包数量在满足minChunks的基础上,还可以通过maxInitialRequest/maxAsyncRequests配置项限制分包数量。配置项的语义为:maxInitialRequest:用于设置InitialChunk的最大并行请求数maxAsyncRequests:用于设置AsyncChunk的最大并行请求数这里所说的“请求数”指的是分包数在加载Chunk时需要同步加载。例如,对于一个ChunkA,如果按照分包规则(如模块引用数、第三方包数)分隔出多个子ChunkA。,那么在请求A时,浏览器需要请求所有的A?,并行请求数等于?一个分包加上一个主包,即?+1。比如上面例子中提到的module关系:如果minChunks=2,commonmodule命中minChunks规则,独立分包。浏览器请求entry-a时,需要同时请求common包,并行请求数为1+1=2。对于如下模块关系:如果minChunks=2,common-1和common-2同时命中minChunks规则,分别打包。浏览器请求entry-b时,需要同时请求common-1和common-2两个子包。并行数为2+1=3,此时如果maxInitialRequest=2,分包数超过阈值,SplitChunksPlugin会放弃common-1和common-2中较小的分包。maxAsyncRequest的逻辑类似,这里不再赘述。并行请求数的关键逻辑总结如下:InitialChunk本身算作一个请求。AsyncChunk不算作并行请求。由runtimeChunk拆分的运行时不算并行请求。如果两个Chunk同时满足拆分规则,但是maxInitialRequests(或maxAsyncRequest)的值只允许再拆分一个module,那么先拆大的module。2.2.3限制分包体积在满足minChunks和maxInitialRequests的基础上,SplitChunksPlugin会进一步判断Chunk包的大小来决定是否分包。与规则相关的配置项有很多:minSize:超过这个大小的Chunk会被官方分包maxSize:超过这个大小的Chunk会继续分包maxAsyncSize:类似maxSize,但只对异步导入的模块有效maxInitialSize:类似maxSize,但只对入口enforceSizeThreshold配置的入口模块有效:超过这个大小的Chunks会被强制分包,忽略上面其他的大小限制。那么结合上面介绍的两条规则,SplitChunksPlugin的主要流程如下:判断Chunk是否满足maxInitialRequests阈值,如果满足则进入下一步判断Chunk资源的体积是否大于上述配置项minSize声明的下限阈值;如果volume"小于minSize,则取消分包,相应的Module仍会合并到原来的Chunk中。如果Chunk的大小"大于"minSize,则判断是否分包超过了maxSize、maxAsyncSize、maxInitialSize声明的上限阈值,如果是,则尝试继续将Chunk分成更小的部分,虽然maxSize等上限阈值逻辑会产生更多的包,但缓存粒度会更小,命中率会降低相对更高。随着持久缓存和HTTP2的复用以上面的模块关系为例:如果此时webpack配置的minChunks大于2,并且maxInitialRequests也大于2,如果公共模块的体积大于上面描述的minxSize配置项,就会分包成功则将commont分离成一个单独的Chunk,否则合并到原来的3个Chunk中。注意这些属性的优先级顺序是:maxInitialRequest/maxAsyncRequests