webpack-SplitChunksPlugin学习

前言

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 还内置了 defaultdefaultVendors 两个 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.js

b.js->common1.js

c.js->common.js+common1.js

如上,a,b,c被视为三个initital chunkm,common1和common都满足minChunks的要求,但是在加载c的chunk的时候,他的分包为common和common1,此时并行数量=1+2=3,并不满足maxInitailRequests=2的需求,此时,SplitChunksPlugin放弃 common-1common-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 核心原理与应用实践》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值