在翻译中学习Webpack-SplitChunksPlugin

4,335 阅读9分钟

SplitChunksPlugin

通常,在webpack的内部图谱里面,chunks是以父子关系关联在一起的。CommonsChunkPlugin曾被用来避免他们之间的重复依赖,但是在未来它将起不到优化作用了。

webpack4开始,CommonsChunkPlugin已经被移除了,取而代之的是optimization.splitChunks

默认场景

对于大部分用户来说,SplitChunkPlugin是开箱即用的,而且会用的很好。

默认情况下,它只会影响到那些按需加载chunks,因为修改initial chunks会影响到项目的HTML文件中的脚本标签。

webpack在以下场景下会去自动分割chunks

  1. 新的chunk可以被多个chunk分享,或者它来自于node_modules文件夹
  2. 新的chunk体积大于30kb(在进行min+gz之前的体积)
  3. 在按需加载chunk时,其最大并发请求的数量小于等于5
  4. 在加载初始化页面过程中,其最大并发请求的数量小于等于3

在尽量满足后两个场景的情况下,更大体积的chunk将更受到SplitChunkPlugin的关注(去优先判断体积)

配置

SplitChunksPlugin默认情况下有一些初始配置,如果你不了解这些默认的初始配置,那么你可能会有一定的困扰。所以,在使用webpack的各种插件之前,先了解其默认配置是一个不错的主意。

optimization.splitChunks

下面这个配置对象就可以解释出SplitChunksPlugin的一些默认行为:

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      automaticNameMaxLength: 30,
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

splitChunks.automaticNameDelimiter

string

默认情况下,webpack会使用chunk的源和名称来为其生成其相应的文件名(例如:vendors~main.js)。这个选项可以让你定制一个连字符,用来生成chunk的文件名。

splitChunks.automaticNameMaxLength

number: 109

这个选项是用来设置SplitChunksPlugin在生成chunk的文件名时可以生成的文件名称的最大字符个数。

splitChunks.chunks

function (chunk) | string

该配置项决定了哪些chunk会被选取以进行优化。当提供了一个字符串,只有allasyncinitial是合法的关键词。
all具有更强大的力量,因为它可以选取那些共享的chunk,即使它们是在同步和非同步chunk之间共享的。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: 'all'
    }
  }
};

或者,你也可以提供一个函数去做更多的控制。这个函数的返回值将决定是否包含每一个chunk

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks (chunk) {
        // exclude `my-excluded-chunk`
        return chunk.name !== 'my-excluded-chunk';
      }
    }
  }
};

splitChunks.maxAsyncRequests

number

当按需加载时可并发请求的最大请求数量

splitChunks.maxInitialRequests

number

在入口文件中可并发请求的最大请求数量

splitChunks.minChunks

number

在分割前,可被多少个chunk分享的最小值

splitChunks.minSize

number

单位byte,生成chunk的最小体积约束。

splitChunks.maxSize

number

我们通过设置maxSize来告诉webpack去把那些体积大于maxSizechunk分割变成更小的部分(无论是全局的optimization.splitChunks.maxSize还是每一个cache groupoptimization.splitChunks.cacheGroups[x].maxSize,甚至那些fallbackoptimization.splitChunks.fallbackCacheGroup.maxSize都遵循这个规则)。这些更小的部分在体积上最少要大于minSize(且接近于maxSize)。这个算法是不可逆转的,且它只会对模块造成局部影响。因此,这在使用长期缓存和不需要记录的场景下非常有用。

maxSize只是一个提示,当所有模块都大于maxSize的时候它是可以被违背的,或者分割的时候可能会违背minSize

在做分割时,如果chunk已经有了名称,那么这个chunk的每个部分都会基于那个名称重新生成新的名称。具体的生成策略依赖于optimization.splitChunks.hidePathInfo配置的值,它将会基于第一个模块的名称或hash给新名称加上key

maxSize选项的本意是和HTTP/2和长效缓存一起使用。这样它将减少请求数量已达到更换的缓存效果。它也可以被用来减小文件的体积以求更快的构建速度。

maxSize相比于maxInitialRequest/maxAsyncRequests具有更高的权重。实际上,它们的权重排序是这样的:maxInitialRequest < maxAsyncRequests < maxSize < minSize

splitChunks.name

boolean = true function (module, chunks, cacheGroupKey) => string

string

改规则同样适用于每一个cacheGroupsplitChunks.cacheGroup.{cacheGroup}.name

如果设置为true,它将基于chunkcache group key自动生成一个名字。

如果设置为一个字符串或者一个函数的话,它将允许你生成一个自定义的名字。尤其是如果你设置的是同一个字符串或者一个函数总是返回相同的字符串,它将自动把所有的通用模块和vendor都合并到一个chunk里面去。这将导致一个比较大的初始化下载,并且加载页面数据会比较慢。

如果你设置了一个函数,那么你将从入参中得到chunk.namechunk.hash属性,这对你去自定义生成chunk的名称尤其有用。

如果splitChunks.name匹配到了一个entry point名称,那么这个entry point将会被移除掉。

在线上环境,splitChunks.name推荐被设置为false,因为它在没必要的情况下不会变更名称。

main.js
import _ from 'lodash'

console.log(_.join(['Hello', 'webpack'], ''))

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          // cacheGroupKey here is `commons` as the key of the cacheGroup
          name(module, chunks, cacheGroupKey) {
            const moduleFileName = module.identifier().split('/').reduceRight(item => item);
            const allChunksNames = chunks.map((item) => item.name).join('~');
            return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
          },
          chunks: 'all'
        }
      }
    }
  }
};

运行以上配置,它将产出一个common groupchunk,名称为commons-main-lodash.js.e7519d2bb8777058fa27.js

当给不同的split chunk设置一个相同的名称,那么所有的vendor模块都会被打包替换到单一的共享chunk中去。尽管它是不被推荐的。

splitChunks.cacheGroups

cacheGroup可以继承或者重写任何splitChunks.*中设置的任何配置项,但是testpriorityreuseExistingChunk只能在cacheGroup下配置。如果想去禁用cacheGroup的所有默认行为,将它们设置为false即可。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false
      }
    }
  }
};
splitChunks.cacheGroups.{cacheGroup}.priority

number

在进项分割时,一个模块可能同时属于不同的cache group,优化器将会选择那个拥有更高prioritycache group。默认的分组拥有一个priority为负数的权重,以利于自定义分组可以设置一个更高的权重(自定义分组的默认值是0)。

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

boolean

如果当前chunk中包含的模块已经从main bundle中分割出来了,那么它将会直接使用那个模块而不是重新生成一个。这个行为可能会影响当前chunk的名称。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          reuseExistingChunk: true
        }
      }
    }
  }
};
splitCHunks.cacheGroups.{cacheGroup}.test

function (module, chunk) => boolean RegExp string

该选项控制着哪些模块将被当前cache group选中。如果忽略这个选项,那么它将默认选择所有模块。它可以匹配模块资源的绝对路径也可以是chunk的名称。当一个chunk的名称匹配上了,那么这个chunk里的所有模块也都被选中了。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test(module, chunks) {
            //...
            return module.type === 'javascript/auto';
          }
        }
      }
    }
  }
};
splitChunks.cacheGroups.{cacheGroup}.filename

string

当且仅当当前这个chunk是一个initial chunk的时候,该选项会允许你去重写其文件名称。所有的占位符都可以在output.filename中找到。

该选项虽说也可以在全局中设置splitChunks.filename,但是它是不推荐的,因为在splitChunks.chunks没有被设置为initial的情况下它会导致一些错误。避免在全局中设置它。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          filename: '[name].bundle.js'
        }
      }
    }
  }
};
splitChunks.cacheGroups.{cacheGroup}.enforce

boolean = false

该选项在设置为true的情况下将导致webpack会忽略splitChunks.minSizesplitChunks.minChunkssplitChunks.maxAsyncRequestssplitChunks.maxInitialRequests选项的值,并且总是为当前这个cache group创建一个chunk

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          enforce: true
        }
      }
    }
  }
};

示例

默认值:示例1

// index.js

import('./a'); // dynamic import
// a.js
import 'react';

//...

结果: 一个分割的chunk将会被创建,且它内部包含着react模块。在import call中,该chunk将会和那个包含着./a的原始chunk一起并行加载。

为什么?

  • 场景1: 当前这个chunk包含着来自于node_modules的模块;
  • 场景2: react的体积大于30kb
  • 场景3: import call的并行请求的数量是2
  • 场景4: 不影响初始化页面也在的请求。

那么它背后的原因是什么呢? react通常不会随着你的应用代码经常变动。通过把它分割到一个独立的chunk中去可以让它脱离你的应用代码独立缓存(假设你正在使用chunkhash, records, Cache-Control或者长效缓存)。

默认值:示例2

// entry.js

// dynamic imports
import('./a');
import('./b');
// a.js
import './helpers'; // helpers is 40kb in size

//...
// b.js
import './helpers';
import './more-helpers'; // more-helpers is also 40kb in size

//...

结果:一个独立的chunk将会被创建,它里面包含着./helpers以及所有它的依赖模块。在import call的时候,该chunk会和原始的chunk一起并行加载。

为什么呢?

  • 场景1: 该chunk被两个import call所共享
  • 场景2: helpers的体积大于30kb
  • 场景3: import call的并行请求个数等于2
  • 场景4: 不影响初始化页面的加载请求

如果把helpers的内容放在每一个chunk中将会导致它的代码被下载两次。但是我们通过一个独立的chunk可以让它只下载一次。我们虽然付出了增加一次请求的代价,但是这是权衡利弊后的结果。 这也是为什么要设置最小尺寸是30kb的原因。

分割Chunk:示例1

创建一个common chunk,它将包含在entry point中所共享的所有代码

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2
        }
      }
    }
  }
};

此配置会增大你的初始化bundles,我们推荐使用动态引入的方式去加载模块,当它不是被立即需要的时候。

分割Chunks:示例2

创建一个vendors chunk,它将包含整个应用中所有来自于node_modules中的所有代码。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

这将导致一个很大的chunk,它包含了所有的扩展包。我们推荐只把你的框架和工具模块分割在一起,而其他的依赖模块通过动态引入。

分割Chunks:示例3

创建一个custom chunk,它将包含所以匹配上了正则表达式的来自于node_modules的模块。

webpack.config.js

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor',
          chunks: 'all',
        }
      }
    }
  }
};

这将导致reactreact-dom模块都分割到同一个独立的chunk中去。