webpack SplitChunksPlugin 配置详解

When ?什么时候进行处理

首先我们要清楚 SplitChunksPlugin 插件侵入 webpack 流程的时机,不然不能很好地理解它的配置参数到底起什么作用。通过源码得知它是在CompilationoptimizeChunksAdvanced钩子触发时进行工作的,而此时 chunk 已经生成并完成了初步优化。

【webpack 之 Chunk】中,我们已经分析过这时 chunk 分包的状态:

  1. 同一个 entry 入口模块与它的同步依赖(直接/间接) 组织成一个 chunk,还包含 runtime (webpackBootstrap 自执行函数的形式)。
  2. 每一个异步模块与它的同步依赖单独组成一个 chunk。其中只会包含入口 chunk 中不存在的同步依赖;若存在同步第三方包,也会被单独打包成一个 chunk。

那么,SplitChunksPlugin 就是在这个基础上再做优化了,也就是对这些 chunk 进行进一步的组合/分割。

Why and How ?为何要代码分割及如何分割

Code Splitting 拆包优化的最终目标是什么?无非是:

  1. 把更新频率低的代码和内容频繁变动的代码分离,把共用率较高的资源也拆出来,最大限度利用浏览器缓存。
  2. 减少 http 请求次数的同时避免单个文件太大以免拖垮响应速度,也就是拆包时尽量实现文件个数更少、单个文件体积更小。

第二点的两个目标是互相矛盾的,因此要达到两者之间的平衡是个博弈的过程,没有太绝对的拆包策略,都是力求提高性能水准罢了。

具体来说,比如一些第三方插件,更新频率其实很低,单个体积通常又较小,就很适合打包在一个文件里。而 UI 组件库更新少的同时体积却比较大,就可以单独打成一个包(也有直接用 CDN 外链的)。还有程序员自己写的公共组件,一般写完后修改也不多,适合拎出来放一个文件。

webpack 配置output.filenameoutput.chunkFilename值中的[contenthash]使得重新打包时若 chunk 内容没有变化,就跳过直接使用缓存,当然对应的输出文件名称中的 hash 值也不会改变。这样既能提高二次构建速度,又能不影响用户的浏览器缓存。
为何配置文件名时在取值占位符里使用[contenthash]而不是[chunkhash]呢?
[chunkhash]是 chunk 级别的 hash 值。但在项目中我们通常的做法是把 css 都抽离出来,作为模块import到对应的 js 文件中。如果使用[chunkhash],两个关联的 js 和 css 文件名的 hash 值是一样的。一旦其中一个改动了,与其关联的另一个文件即使毫无变化,文件名也会改变,缓存也就失效了。
[contenthash],只关注文件本身,自身内容不变,hash 值也不会变。

其余剩下的基本就是我们的业务代码,改动频率就很大了,是每次发布版本都会变的。

常用的代码分离方法

  • 入口起点:通过 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 内置插件 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过异步引入模块(如import('./m.js'))来分离代码。

webpackChunkName

异步加载的 chunk 无法通过 webpack 配置自定义打包后的名称,默认都是以0、1、2...这样的数字命名。
魔法注释可以帮助我们自定义异步 chunk 名。

component: () => import(/* webpackChunkName: "route-login" */ '@/views/login')

如果想把某个路由下的所有组件都打包在同一个异步块 (chunk) 中。那么在webpackChunkName注释提供相同的 chunk name 即可。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

SplitChunksPlugin 配置详解

先看下SplitChunksPlugin插件的默认配置,再结合实例来搞懂每个配置项真正的用处。
webpack 上的文档地址:【SplitChunksPlugin API】

module.exports = {
  // 加上入口和输出的配置,以便结合实际说明
  entry: {
    index: './src/a',
    admin: './src/b'
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].[contenthash:6].js',
    chunkFilename: '[name].[contenthash:8].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'async', // 2. 处理的 chunk 类型
      minSize: 20000, // 4. 允许新拆出 chunk 的最小体积
      minRemainingSize: 0,
      minChunks: 1, // 5. 拆分前被 chunk 公用的最小次数
      maxAsyncRequests: 30, // 7. 每个异步加载模块最多能被拆分的数量
      maxInitialRequests: 30, // 6. 每个入口和它的同步依赖最多能被拆分的数量
      enforceSizeThreshold: 50000, // 8. 强制执行拆分的体积阈值并忽略其他限制
      cacheGroups: { // 1. 缓存组
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, // 1.1 模块路径/文件名匹配正则
          priority: -10, // 1.2 缓存组权重
          reuseExistingChunk: true, // 1.3 复用已被拆出的依赖模块,而不是继续包含在该组一起生成
        },
        default: {
          minChunks: 2, // 5. default 组的模块必须至少被 2 个 chunk 共用 (本次分割前) 
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
1. cacheGroups

核心配置 - 缓存组,可以继承/覆盖来自splitChunks.*的任何选项。它自身拥有testpriorityreuseExistingChunk 三个配置项。
SplitChunksPlugin 就是根据cacheGroups去拆分模块的,后面2. 3. ...等其余属性其实是应用到每一个缓存组的公共配置,同样的参数以缓存组的值为准

把默认缓存组defaultVendorsdefault设置为 false,即可禁用对应缓存组规则。
模块必须符合某个缓存组的所有条件,才会被分割。

  • 1.1 test 模块匹配规则,可以匹配模块资源绝对路径(函数或正则)或 chunk 名称(字符串),匹配 chunk 名称时(如'app'),将选择 chunk 中的所有模块。
    可选值:function (module, { chunkGraph, moduleGraph }) => boolean | RegExp | string
cacheGroups: {
  chunks: 'all',
  react: { // 1. 正则匹配示例,把 react 和 react-dom 分到一个名为 `lib-react` 的 js 中
    // `[\\/]` 是作为跨平台兼容性的路径分隔符
    test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
    name: 'lib-react',
  },
  svgIcon: { // 2. 函数匹配示例,把自定义 svg 图标都拆出来,放到 `svgIcon.js` 中
    test(module) {
      // `module.resource` 是文件的绝对路径
      // 用`path.sep` 代替 / or \,以便跨平台兼容
      const path = require('path'); // path 一般会在配置文件引入,此处只是说明 path 的来源,实际并不用加上
      return ( // 匹配 icon 文件夹下的 .svg 后缀文件
        module.resource &&
        module.resource.endsWith('.svg') &&
        module.resource.includes(`${path.sep}icons${path.sep}`)
      );
    },
    name: 'svgIcon'
  },
},
  • 1.2 priority
    默认值:-20
    缓存组打包的优先级/权重,数值大的优先。
    一个模块同时满足多个缓存组的条件,会优先考虑权重最高的那个缓存组。
    默认组的优先级为负,以允许自定义组获得更高的优先级 (自定义组的默认值为0)。
  • 1.3 reuseExistingChunk
    默认值:true
    如果当前 chunk 包含已从主 chunk 中拆分出的模块,那么缓存组不会在新 chunk 内生成这个/些模块,而是去复用被拆出的 module。
    这可能会影响 chunk 的结果文件名。
2. chunks

默认值:"async"
选择进行代码分割的 chunk 类型,可选值:"all" | "async"(异步) | "initial"(同步)

默认配置只会对按需加载的代码进行分割。那么入口文件同步依赖的第三方包和公共模块是无法拆出来的。因此通常会将SplitChunk整体的值设置为"all",把初始加载的代码也加入到分割的“受众”中来。在具体缓存组如有需要再按实际情况再覆盖。
如果设为"initial",那么该缓存组只会分离应用初始加载需要的包。有时这是有必要的,因为设为一味设为"all"的话,打包出来的 js 都会在应用初始载入时加载,即使里面包含一些首页用不到的模块。

3. automaticNameDelimiter

默认值:'~'
此选项可以指定生成名称中的分隔符。

默认情况下,若未用cacheGroups.{cacheGroup}.name自定义 chunk 名, webpack 会使用 chunk 的缓存组名和entry来源生成 chunk 名(例如default~index.jsdefaultVendors~admin.js)。

4. minSize

默认值:20000
生成 chunk 的最小体积 (以bytes为单位)

新拆出的 chunk 的体积最小值,也就是符合缓存组其他条件的前提下,体积大于等于这个值的模块/模块集合才会被拆分出来。
比如我们有两个入口 chunk,各自都包含了一个模块m(或者均有m1m2),本来符合默认配置中的default缓存组,但由于这个模块(或者m1加上m2)体积不足 20kb,便无法被输出为一个文件。

⚠️ 即使不匹配任何一个缓存组,在 splitChunks.* 的minSize选项会影响异步 chunk。规则是体积大于minSize值的公共模块会被拆出。(除非 splitChunks.* chunks: 'initial',才没有这种影响)
公共模块即 >= 2个异步 chunk 共享的模块,同minChunks: 2

5. minChunks

默认值:1
拆分前必须共享模块的最小 chunks 数

比如数值是2,那么在符合某个缓存组其他规则的前提下,拆分前必须有 2 个 chunk 共用了这个模块,才可以被归到这个组下拆分出来。
不是文件共享而是 chunk 共享,所以清楚 SplitChunksPlugin 处理前 chunk 的分包情况非常有必要。

6. maxInitialRequests

默认值:30
每个入口点的最大并行请求数。

也就是每个入口和它的同步依赖最多够被拆分/合并成几个js文件。对这个数量进行限制为的是避免初始js请求过多。

注意几点:

  • 入口文件本身算一个请求
  • 如单独拆出了runtimeChunk,不算在内
  • 单独拆出的css文件不算在内
  • 若同时有两个模块满足cacheGroup规则要进行拆分,但maxInitialRequests只允许再拆出一个文件,那么体积较大的模块会被拆分出来。
7. maxAsyncRequests

默认值:30
每个按需加载模块的最大并行请求数。

也就是一个异步加载模块和它的同步依赖最多能被拆分成几个js。除了处理对象不同,应该很好理解。

注意几点:

  • import()文件本身算一个请求
  • 同样不算js以外的公共资源请求如css
  • 若同时有两个模块满足cacheGroup规则要进行拆分,但maxAsyncRequests只允许再拆出一个文件,那么体积较大的模块会被拆分出来。
8. enforceSizeThreshold

默认值:50000
如果符合缓存组其他条件(不包括下面三项)的模块/模块集超过这个体积阈值,就忽略minRemainingSize, maxAsyncRequests, maxInitialRequests的配置,总是为这个缓存组创建 chunk。
也就是说即使超出了maxAsyncRequestsmaxInitialRequests指定的可拆分次数,只要缓存组模块体积大于50kb,仍然会分出新 chunk。

干货讲完,实战另开一篇一面:【webpack SplitChunksPlugin vue-cli 4 拆包实战】

参考文章:
webpack高级概念code splitting 和 splitChunks (系列五)
有点难的知识点: Webpack Chunk 分包规则详解

你可能感兴趣的:(webpack SplitChunksPlugin 配置详解)