Webpack v4 chunk 分块深度挖掘
补充理解
一.
module/chunk/bundle的理解
- module: 模块,在webpack眼里,任何可以被导入导出的文件都是 一个模块
- chunk: chunk是webpack拆分出来的:
每个入口文件都是一个chunk
通过 import、require 引入的代码也是
通过 splitChunks 拆分出来的代码也是 - bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出
注:Chunk是过程中的代码块,Bundle是结果的代码块
二.
initial: 所有的立即加载的chunk(例如bundle文件)将被检查
async: 所有延迟加载的chunk将被检查
all: 等价于initial + async效果,所有的chunk都将被检查
(chunkname) => boolean: 函数形式可以提供更细粒度的控制
三.
chunk-vendors.js,顾名思义,是所有不属于您自己,而是来自其他方的模块的捆绑包 。它们被称为第三方模块,或 vendor 模块。
通常,它意味着(仅和)来自您项目的 /node_modules 目录的所有模块
正文
TL:DR-使用2018年11月25日发布的webpack v4.26.1以及优化。
optimization.splitChunks options: chunks: ‘all’,
splitChunks.minSize and splitChunks.maxSize。
如果您使用HTTP/2并希望对chunk创建进行更多控制,或者您对chunk名称很挑剔,请使用chunks:'initial’并相应地指定您自己的命名
更新2018-12-04:Webpackv4.27.0刚刚发布,它修复了与maxSize相关的一个剩余问题,并引入了一个轻微的行为变化,这在本文底部的附录中进行了解释
这篇博客文章将关注Webpack配置的一个关键领域,即如何将你的bundle拆分成更小的块。
如果您是webpack新手,我建议您阅读一些教程和文档https://webpack.js.org/concepts/在继续阅读本文之前,请先对构建系统有一个基本的了解。
有很多好的博客文章可以通过快速搜索找到,涵盖了webpack基础知识,所以我不会试图在这里重新发明轮子。
Webpack v3和v4之间有很多变化,因此最好尝试找到涵盖Webpack v4的方法。许多在Webpack v3中工作的配置也可以在v4中工作,但不是最佳配置,可能会被弃用
下面的许多示例至少依赖于Webpack v4.16.0
“chunk”是一个文件,它构成了你的应用程序的JS bundle的一部分,它们是由bundle拆分过程创建的,如果没有bundle拆分,你的所有应用程序代码都将在一个JSbundle文件中。
每个chunk通常是一个包含一个或多个应用程序JavaScript module的JS文件。
每个chunk都需要通过script标签在index.html文件中被连接,这通常通过使用“HtmlWebpackPlugin”实现。(备注:可以通过HtmlWebpackPlugin插件可以生成一个html文件,并将打包好的文件引入到html中)注意:如果使用“MiniCssExtractPlugin”,一个chunk也可以是CSS文件(备注:MiniCssExtractPlugin插件可以将css单独抽离出来,形成一个单独的文件)
自Webpack v4以来,index.html中列出的chunk的顺序无关紧要,因为Webpack运行时在下载所有初始chunk(下面解释初始chunk)之前不会执行代码。
我在任何文档中都找不到对此的引用,但通过注释掉一个不重要的chunk进行测试表明,这是一个问题,因为它破坏了应用程序,没有执行JS代码
那么,一个JS文件bundle有什么问题?有两件事,可缓存性和可靠性。
大多数JavaScript框架(如React、Angular和Aurelia)都非常大,所有的应用程序和框架代码都放在一个JS文件中,这意味着对发布的应用程序代码的任何小更新都需要再次下载一个大的JS bundle文件,才能将更新的代码交付给浏览器。
如果代码分布在许多较小的文件中,我们可以利用web浏览器缓存。此外,通过较慢且不太可靠的连接(如移动数据)下载大型文件比下载小型文件更容易失败
SplitChunks Plugin
自webpack v4以来,CommonsChunkPlugin已被删除,取而代之的是SplitChunkPlugin,配置由webpack配置选项提供:optimization.splitChunks
公告:https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693
文档链接: https://webpack.js.org/plugins/split-chunks-plugin/
在文档中记录一句话
默认情况下,它仅影响按需块,因为更改初始块会影响HTML文件应包含的脚本标签以运行项目(这段话仅适用于不使用HtmlWebpackPlugin插件将script标签注入index.html的项目)
initial chunk是指为静态导入的modules创建的chunk,这是最常见的用法:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
on-demand chunk是指为异步加载(延迟加载)modules创建的chunk,这些modules使用动态导入加载,https://developers.google.com/web/updates/2017/11/dynamic-import
考虑到这一点,有两件重要的事情要做
1.使用HtmlWebpackPlugin,这样我们就可以分割初始块,而无需不断更新index.html,以避免在添加模块时破坏应用程序
2.更新Webpack配置,使SplitChunksPlugin同时影响“初始”和按需(异步)块
以下配置将拆分“初始”和“异步”块
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
chunks: 'all' // options: 'initial', 'async' , 'all'
}
}
注意:'runtimeChunk:true’和’moduleIds:“hashed”'设置对于长期可缓存性很重要:https://developers.google.com/web/fundamentals/performance/webpack/use-long-term-caching请注意,Webpack v4.16.0中已弃用上述链接中提到的HashedModuleIdsPlugin/“optimization.hashedModuleIds”https://webpack.js.org/configuration/optimization/#optimization-moduleids
上述配置将导致(取决于您的项目)一个单独的“vendor”chunk,其中包含“node_modules”目录中的所有内容,这意味着任何JavaScript Framework代码都与应用程序代码保持独立。通过查看默认的cacheGroups配置来解释此行为:
https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks
当我说根据您的项目,如果所有节点模块的总和小于30k,则不会创建单独的“vendors”chunk。这是由splitChunks.minSize设置(默认值为30000)决定的。该设置设置了一条规则,规定除非chunk的大小至少为minSize,否则不应创建块。请注意,在进行任何缩小之前,将大小与模块大小进行比较
cacheGroups配置决定了如何将Webpack拆分为多个chunk,默认的cacheGroups提供了最小的设置。只要使用chunks:‘all’,不使用动态导入的项目将以一个“main”chunk和一个单独的“vendors”chunk结束。这样做的问题是,“vendors”chunk通常是包的最大部分,需要进一步拆分以避免大的chunk
另一个重要的配置是指定chunk的命名
https://webpack.js.org/configuration/output/#output-filename
output: {
filename: '[name].[chunkhash].bundle.js'
chunkFilename: '[name].[chunkhash].chunk.js'
},
上面的配置为chunk命名,以便我们可以看到它们是从optimization.splitChunks 配置的哪个部分创建的,并包括基于每个块内容的hash。chunkhash用作缓存破坏技术,强制浏览器重新下载包含自上次构建以来已更改的模块的任何块
大的chunk会有什么问题?
1.通过不可靠的连接(如移动数据)下载大型文件更容易失败。
2.Webpack支持构建包以实现长期可缓存性,如果所有模块都在一个chunk中,这将无法正常工作
那么为什么不让每个模块都在自己的chunk中呢?过多的chunk将导致更长的下载时间,就像HTTP/1.1一样,因为浏览器只能发出6个并行请求。由于连接到服务器的延迟,进一步的请求将增加一个小的延迟,每个请求在将实际文件数据发送到浏览器之前都会占用少量时间,这在大量请求的情况下会变得非常重要。每个请求在HTTP头和协议数据方面也有一些开销
HTTP/2
使用HTTP/2意味着可以并行下载许多chunk,而不会导致延迟。这是因为HTTP/2使用多路复用,避免了多个请求的延迟和额外的数据开销
考虑到这一点,很明显Webpack splitChunks配置需要针对HTTP/1.1或HTTP/2进行优化
HTTP/1.1的优化意味着将块限制在理想情况下不超过6个(尽管要在总体下载时间上产生显著差异,需要远远超过6个)。
HTTP/2的优化意味着要创建许多chunk,但不能太多,否则bundle的整体大小会受到太大的影响。(拆分会产生较小的开销)。
在7月份发布webpackv4.15.0之前,使用“splitChunks”将bundle拆分为更小的块的唯一方法是创建cacheGroups,以将模块分组为多个单独的块。下面的配置示例将jQuery拆分为一个名为“vendor.jQuery”的单独块,其余的node_module由内置的catch all“vendors”chunk捕获。
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
chunks: 'all' // options: 'initial', 'async' , 'all'
cacheGroups: {
jquery: {
test: /[\\/]node_modules[\\/]jquery[\\/]/,
name: 'vendor.jquery',
enforce: true, // create chunk regardless of the size of the chunk
priority: 90
}
}
}
}
注意:优先级需要为零或更高,以便在具有负优先级的内置cacheGroups之前处理。“jquery.test”属性正在指定正则表达式以匹配模块的路径。
或者,我们可以禁用内置的cacheGroup,并创建自己的“vendors”cacheGroup:
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
chunks: 'initial',
cacheGroups: {
default: false, // disable the built-in groups, default & vendors (vendors is overwritten below)
jquery: {
test: /[\\/]node_modules[\\/]jquery[\\/]/, // matches /node_modules/jquery/
name: 'vendor.jquery',
enforce: true,
priority: 90
}
vendors: {
test: /[\\/]node_modules[\\/]/, // matches /node_modules/
name: 'vendors',
priority: 10,
enforce: true, // create chunk regardless of the size of the chunk
}
}
}
}
根据您的项目,上述配置应该会产生4个chunk:“runtime”、“main”(应用程序块)、“vendor.jquery”和“vendors”。其他大的模块也可以使用上述方法从“vendors”chunk中分离出来
我最近使用HTTP/1.1和HTTP/2对各种splitChunks配置的影响进行了一些测试,可以在下面的评论中阅读这些测试的摘要:https://github.com/aurelia/cli/issues/969#issuecomment-438692909
幸运的是,Webpackv4.15.0引入了一个新选项:“splitChunks.MaxSize”,这证明非常有用。该选项指定单个块的大小限制,如果单个块由多个模块组成,则将其拆分为较小的部分以尝试满足maxSize。一个额外的hash被添加到块文件名中,以使其唯一。如果单个模块大于maxSize,它将在自己的块中结束
https://webpack.js.org/plugins/split-chunks-plugin/#splitchunks-maxsize
我将在这里提出的建议是基于我自己的测试以及其他调查工作。
在测试maxSize选项时,我发现并报告了一些问题,这些问题现已在Webpack v4.26.1版本中得到解决:https://github.com/webpack/webpack/issues/8407
值得阅读上述问题讨论,因为仍有可能遇到边缘情况。
使用splitChunks.maxSize
主要的一些基本配置
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
chunks: 'all', // options: 'initial', 'async' , 'all'
maxSize: 200000 // size in bytes
}
}
与本文开头的第一个示例一样,如果所有节点模块的总和大于minSize(默认值为30000字节),则上述配置将产生一个单独的“vendor”块,其中包含“node_modules”目录中的所有内容。但现在使用maxSize,如果该块大于200KB(确切地说是195.3KB),则会将其拆分为多个“vendors”块
要创建更多有意义的名称,您需要指定一些命名的缓存组,例如:
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
hidePathInfo: true, // prevents the path from being used in the filename when using maxSize
chunks: 'initial', // options: 'initial', 'async' , 'all'
//minSize: 30000, // default is 30000 bytes
maxSize: 200000, // size in bytes
cacheGroups: {
default: false, // disable the built-in groups, default & vendors (vendors is overwritten below)
vendors: { // picks up everything from node_modules as long as the sum of node modules is larger than minSize
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 19,
enforce: true, // causes maxInitialRequests to be ignored, minSize still respected if specified in cacheGroup
minSize: 30000 // use the default minSize
},
vendorsAsync: { // vendors async chunk, remaining asynchronously used node modules as single chunk file
test: /[\\/]node_modules[\\/]/,
name: 'vendors.async',
chunks: 'async',
priority: 9,
reuseExistingChunk: true,
minSize: 10000 // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
},
commonsAsync: { // commons async chunk, remaining asynchronously used modules as single chunk file
name: 'commons.async',
minChunks: 2, // Minimum number of chunks that must share a module before splitting
chunks: 'async',
priority: 0,
reuseExistingChunk: true,
minSize: 10000 // use smaller minSize to avoid too much potential bundle bloat due to module duplication.
}
}
}
}
请注意,在上面的配置中,正在使用chunks:“initial”,这是因为异步加载(延迟加载)的模块由“vendorsAsync”和“commonsAsync“cacheGroups单独拾取。这在使用动态模块导入时尤为重要。
“vendors”cacheGroup表示,如果所有静态导入的节点模块加起来都达到30KB或更大,请将它们与应用程序块分离,并将它们全部放在“vendor”块中。该配置还指出,如果该“vendors”chunk大于200KB,请将其拆分为多个“vendors”。使用“enforce:true”会导致忽略splitChunks.maxInitialRequests、splitChucks.maxAsyncRequests和splitChunk s.minSize,但是,如果在cacheGroup上也指定了minSize,则在创建块的决策中会考虑minSize,并且在使用maxSize时也会在拆分逻辑中使用minSize。
上述示例中的“vendorsAsync”cacheGroup包含应用程序异步加载模块中导入的所有节点模块。
上述示例中的“commonsAsync”cacheGroup类似于此处显示的内置“default”cacheGroup:https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunk,它覆盖应用程序的任何模块,这些模块由至少两个异步加载的应用程序模块导入(认为是共享的)。(“vendorsAsync”已拾取“node_modules”目录中的任何模块)
什么是async chunk?
“async”意味着cacheGroup只能包含在异步加载的模块中导入的模块。“在异步加载的模块中导入”这一措辞很重要,因为实际的异步加载模块本身(通过使用动态导入(如import(‘some-module’/webpackChunkName:‘some-module’/)创建)总是使用魔术注释中指定的块名称放入其自己的异步块中。
实际上,“异步”cacheGroup包含在动态(异步)加载的模块中静态导入的模块。如果一个模块被动态导入到一个动态导入的模块中,那么它将自动以与其父模块相同的方式终止于它自己的单独异步块中,而不考虑任何splitChunks配置。
如果我们没有在上面的示例中指定“异步”cacheGroup,那么在动态(异步)“某个模块”中导入的任何内容都将保留“某个”异步块的一部分(取决于maxSize)。在上面的示例中,如果另一个异步加载的模块使用相同的导入,“commonsAsync”cacheGroup允许分割这些导入(取决于minSize),这可以防止跨异步块复制模块。
在上面的示例中,“vendorsAsync”cacheGroup不关心有多少应用程序模块共享一个节点模块,默认的minChunks为1,这意味着在异步加载的应用程序模块中导入的节点模块将始终放入“vendorsAsync”块中,取决于minSize。这是一个武断的决定,无论是否共享,都要将vendors代码与应用程序代码分开。
minSize and sync chunks
通过在10KB的异步cacheGroups上使用较小的minSize,意味着只有在大于10KB而不是默认的30KB时,模块才会被拆分并放入“vendorsAsync”/“commonsAsync“块中。这意味着,当在多个异步加载的模块中使用时,小于10KB minSize的应用模块将被包含并跨异步块复制。
在上面的示例中,异步cacheGroups上的minSize设置是在使用异步加载的模块时,模块复制和获取所有所需块所需的请求数量之间的权衡。异步加载模块的一个典型用例是Aurelia/Aangular应用程序中的视图,如果这样配置,则在导航到视图时会请求这些视图。
如果视图的所有依赖项都包含在该视图的异步块中,则导航到异步视图只需要一个请求,这样做的代价是,异步块会更大,并且包含可能已经作为其他异步块的一部分下载的模块。
将同一模块放置在多于1个块中会对bundle大小和网络流量造成不利影响,因此在上面的示例中使用了较低的minSize,因此仅适用于较小的模块。为异步缓存组调整minSize可以防止创建小型异步块,从而防止在使用和导航应用程序时需要多个请求。多个请求只是HTTP/1.1的一个问题,使用HTTP/2意味着只需要一个请求就可以检索所需的异步块。
注意:“prefetch”可以用于指示浏览器在浏览器空闲时间提前请求异步块,但这不在本篇文章的范围内,但可能会在未来的文章中介绍。
Optimising the bundle for HTTP/2
优化HTTP/2实际上更多的是在不可靠的连接上实现更好的可缓存性和更高的可靠性。
这是因为,对于一个针对HTTP/1.1进行了优化的应用程序包,使用HTTP/2不会对整个包的下载时间产生明显的影响。只有当bundle包含大量小文件时,差异才会开始变得可衡量。
要调整HTTP/2的WebpacksplitChunks配置,我们应该调整minSize/maxSize以创建更多的块,并添加一些配置以将较大的节点模块放置到它们自己的命名块中
从webpack文档摘取:
“maxSize参数用于HTTP/2和长期缓存。它增加了请求计数,以获得更好的缓存。它还可以用于减小文件大小,以便更快地重建。“
随着拆分次数的增加,更新单个模块将导致重新发布应用程序后需要下载的代码减少,因为包含更新模块的同一块文件中的其他模块将减少。
此示例使用10KB和40KB作为最小和最大大小,并为节点模块创建单独的拆分:
optimization: {
runtimeChunk: true,
moduleIds: 'hashed',
splitChunks: {
hidePathInfo: true, // prevents the path from being used in the filename when using maxSize
chunks: 'initial', // default is async, set to initial and then use async inside cacheGroups instead
maxInitialRequests: Infinity, // Default is 3, make this unlimited if using HTTP/2
maxAsyncRequests: Infinity, // Default is 5, make this unlimited if using HTTP/2
// sizes are compared against source before minification
minSize: 10000, // chunk is only created if it would be bigger than minSize
maxSize: 40000, // splits chunks if bigger than 40k, added in webpack v4.15
cacheGroups: { // create separate js files for bluebird, jQuery, bootstrap, aurelia and one for the remaining node modules
default: false, // disable the built-in groups, default & vendors (vendors is overwritten below)
// generic 'initial/sync' vendor node module splits: separates out larger modules
vendorSplit: { // each node module as separate chunk file if module is bigger than minSize
test: /[\\/]node_modules[\\/]/,
name(module) {
// Extract the name of the package from the path segment after node_modules
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `vendor.${packageName.replace('@', '')}`;
},
priority: 20
},
vendors: { // picks up everything else being used from node_modules that is less than minSize
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 19,
enforce: true // create chunk regardless of the size of the chunk
},
// generic 'async' vendor node module splits: separates out larger modules
vendorAsyncSplit: { // vendor async chunks, create each asynchronously used node module as separate chunk file if module is bigger than minSize
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `vendor.async.${packageName.replace('@', '')}`;
},
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
minSize: 5000 // only create if 5k or larger
},
vendorsAsync: { // vendors async chunk, remaining asynchronously used node modules as single chunk file
test: /[\\/]node_modules[\\/]/,
name: 'vendors.async',
chunks: 'async',
priority: 9,
reuseExistingChunk: true,
enforce: true // create chunk regardless of the size of the chunk
},
// generic 'async' common module splits: separates out larger modules
commonAsync: { // common async chunks, each asynchronously used module as a separate chunk files
name(module) {
// Extract the name of the module from last path component. 'src/modulename/' results in 'modulename'
const moduleName = module.context.match(/[^\\/]+(?=\/$|$)/)[0];
return `common.async.${moduleName.replace('@', '')}`;
},
minChunks: 2, // Minimum number of chunks that must share a module before splitting
chunks: 'async',
priority: 1,
reuseExistingChunk: true,
minSize: 5000 // only create if 5k or larger
},
commonsAsync: { // commons async chunk, remaining asynchronously used modules as single chunk file
name: 'commons.async',
minChunks: 2, // Minimum number of chunks that must share a module before splitting
chunks: 'async',
priority: 0,
reuseExistingChunk: true,
enforce: true // create chunk regardless of the size of the chunk
}
}
}
},
在这里,我们将maxInitialRequests和maxAsyncRequests设置为无限大,以便这些设置不会阻止“vendorSplit”cacheGroup按预期工作,在前面使用“force:true”的示例中不需要这样做。
vendorSplit”cacheGroup为每个节点模块创建块,但仍然看好minSize,这样我们就不会得到太多非常小的块。这一点很重要,因为每个块都会带来少量的bundle size开销,因此在与bundle size进行平衡时,为了缓存性而进行拆分会导致回报减少。
任何小于minSize的节点模块都由“vendor”cacheGroup收集,如前一个示例中所使用的,这次使用“force:true”而不使用minSize,因此创建块,这使节点模块始终与主应用块分离。
上述示例中的’async’ cacheGroups被类似地配置为为每个模块创建单独的块,然后将剩余的小模块聚集到单个块中(如果最终大于maxSize,则为多个)。
创建许多较小的块的唯一缺点是整个包的大小略有增加,所以我们通常会有一个折中的办法来平衡。
根据项目的大小,需要调整最小和最大大小,以实现所需的块数和模块分离程度。
总结
Webpack是一个强大而灵活的工具,正确理解它需要付出一些努力,但回报是值得的。v4.x版本已经做出了很大的改进,并且还在不断进行进一步的改进。
这里介绍的配置的工作示例可以在以下repo中找到:https://github.com/chrisckc/aurelia-cli-skeleton-typescript-webpack
“lazy-loaded-views”分支演示了异步块配置。
更新/附录
更新2018-12-04:Webpack v4.27.0已发布:https://github.com/webpack/webpack/releases/tag/v4.27.0
我的问题日志中的最后一个问题是:https://github.com/webpack/webpack/issues/8407已解决:https://github.com/webpack/webpack/pull/8451并在v4.27.0中发布
这一变化意味着,当在cacheGroup上指定maxSize时,拆分块时将考虑splitChunks中的minSize,之前这需要在cacheGroup中指定minSize。