产物拆包
一、认识
Webpack
默认会将尽可能多的模块代码打包在一起,一般来说,如果不对产物进行代码分割(或者拆包),全部打包到一个 chunk
中, 优点是能减少最终页面的 HTTP
请求数,但缺点也很明显:
-
页面初始代码包过大,影响首屏渲染性能。也就是说: 首屏加载的代码体积过大,即使是当前页面不需要的代码也会进行加载。
-
无法有效应用浏览器缓存,特别对于
NPM
包这类变动较少的代码,业务代码哪怕改了一行都会导致NPM
包缓存失效。也就是说: 线上缓存复用率极低,改动一行代码即可导致整个bundle
产物缓存失效。
代码分离是 webpack
中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle
中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle
,以及控制资源加载优先级,如果使用合理,会极大影响加载时 间。有三种常用的代码分离方法:
-
入口起点:使用
entry
配置手动地分离代码 -
动态导入:通过模块的内联函数调用来分离代码
-
防止重复:使用
SplitChunksPlugin
去重和分离chunk
二、entry
webpack.config.js
配置entry
,配置如下所示:
const Path = require("path");
module.exports = {
mode: "production",
entry: {
entry1: Path.resolve(__dirname, "src", "entry1.js"),
entry2: Path.resolve(__dirname, "src", "entry2.js"),
},
output: {
filename: "[name].js",
path: Path.resolve(__dirname, "build"),
},
};
通过入口起点进行代码分离带来的问题:
- 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。
三、import
当涉及到动态代码拆分时,Webpack
提供了两个类似的技术。对于动态导入,
-
第一种、使用符合
ECMAScript
提案的 import() 语法(优先选择): 以入口文件index.js
引入lodash.js
第三方模块、util.js
本地模块为例:(async function () {
const {
default: { concat },
} = await import(/* webpackChunkName: "lodash" */ "lodash");
console.log(concat);
const { foo1 } = await import(/* webpackChunkName: "util" */ "./utils");
foo1();
})(); -
第二种、使用
webpack
特定的require.ensure
四、SplitChunksPlugin
4.1 Chunk
Chunk
是 Webpack
内部一个非常重要的底层设计,用于组织、管理、优化最终产物,在构建流程进入生成(Seal
)阶段后:
-
Webpack
首先根据entry
配置创建若干Chunk
对象; -
遍历构建(
Make
)阶段找到的所有Module
对象,同一Entry
下的模块分配到Entry
对应的Chunk
中 -
遇到异步模块则创建新的
Chunk
对象,并将异步模块放入该Chunk
-
分配完毕后,根据
SplitChunksPlugin
的启发式算法进一步对这些Chunk
执行裁剪、拆分、合并、代码调优,最终调整成运行性能(可能)更优的形态 -
最后,将这些
Chunk
一个个输出成最终的产物(Asset
)文件,编译工作到此结束
可以看出,Chunk
在构建流程中起着承上启下的关键作用 —— 一方面作为 Module
容器,根据一系列默认 分包策略 决定哪些模块应该合并在一起打包;另一方面根据 splitChunks
设定的策略优化分包,决定最终输出多少产物文件。
Chunk
分包结果的好坏直接影响了最终应用性能,Webpack
默认会将以下三种模块做分包处理:
-
Initial Chunk
:entry
模块及相应子模块打包成Initial Chunk
; -
Async Chunk
: 通过import('./xx')
等语句导入的异步模块及相应子模块组成的Async Chunk
; -
Runtime Chunk
:运行时代码抽离成Runtime Chunk
,可通过entry.runtime
配置项实现。
Initial Chunk
与 Async Chunk
这种略显粗暴的规则会带来两个明显问题:
-
模块重复打包: 假如多个
Chunk
同时依赖同一个Module
,那么这个Module
会被不受限制地重复打包进这些Chunk
-
资源冗余 & 低效缓存:
Webpack
会将Entry
模块、异步模块所有代码都打进同一个单独的包,这在小型项目通常不会有明显的性能问题,但伴随着项目的推进,包体积逐步增长可能会导致应用的响应耗时越来越长。归根结底这种将所有资源打包成一个文件的方式存在两个弊端:-
资源冗余: 客户端必须等待整个应用的代码包都加载完毕才能启动运行,但可能用户当下访问的内容只需要使用其中一部分代码
-
缓存失效: 将所有资源达成一个包后,所有改动 —— 即使只是修改了一个字符,客户端都需要重新下载整个代码包,缓存命中率极低
-
这两个问题都可以通过更科学的分包策略解决,例如:
-
将被多个
Chunk
依赖的包分离成独立Chunk
,防止资源重复; -
node_modules
中的资源通常变动较少,可以抽成一个独立的包,业务代码的频繁变动不会导致这部分第三方库资源缓存失效,被无意义地重复加载。
为此,Webpack
专门提供了 SplitChunksPlugin
插件,用于实现更灵活、可配置的分包,提升应用性能。
4.2 SplitChunksPlugin
SplitChunksPlugin
是 Webpack 4
之后内置实现的最新分包方案,与 Webpack3
时代的 CommonsChunkPlugin
相比,它能够基于一些更灵活、合理的启发式规则将 Module
编排进不同的 Chunk
,最终构建出性能更佳,缓存更友好的应用产物。
SplitChunksPlugin
的用法比较抽象,算得上 Webpack
的一个难点,主要能力有:
-
SplitChunksPlugin
支持根据Module
路径、Module
被引用次数、Chunk
大小、Chunk
请求数等决定是否对Chunk
做进一步拆解,这些决策都可以通过optimization.splitChunks
相应配置项调整定制,基于这些能力我们可以实现:-
单独打包某些特定路径的内容,例如
node_modules
打包为vendors
; -
单独打包使用频率较高的文件
-
-
SplitChunksPlugin
还提供了optimization.splitChunks.cacheGroup
概念,用于对不同特点的资源做分组处理,并为这些分组设置更有针对性的分包规则; -
SplitChunksPlugin
还内置了default
与defaultVendors
两个cacheGroup
,提供一些开箱即用的分包特性:-
node_modules
资源会命中defaultVendors
规则,并被单独打包; -
只有包体超过
20kb
的Chunk
才会被单独打包; -
加载
Async Chunk
所需请求数不得超过30
; -
加载
Initial Chunk
所需请求数不得超过30
-
4.3 配置分包范围
首先,SplitChunksPlugin
默认情况下只对 Async Chunk
生效,我们可以通过 splitChunks.chunks
调整作用范围,该配置项支持如下值:
-
字符串
all
:对Initial Chunk
与Async Chunk
都生效,建议优先使用该值; -
字符串
initial
:只对Initial Chunk
生效; -
字符串
async
:只对Async Chunk
生效; -
函数 (
chunk
) =>boolean
:该函数返回true
时生效;
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'all',
},
},
}
设置为 all
效果最佳,此时 Initial Chunk
、Async Chunk
都会被 SplitChunksPlugin
插件优化。
4.4 根据 Module 使用频率分包
SplitChunksPlugin
支持按 Module
被 Chunk
引用的次数决定是否分包,借助这种能力我们可以轻易将那些被频繁使用的模块打包成独立文件,减少代码重复。
用法很简单,只需用 splitChunks.minChunks
配置项设定最小引用次数,例如:
module.exports = {
//...
optimization: {
splitChunks: {
// 设定引用次数超过 2 的模块才进行分包
minChunks: 2
},
},
}
注意,这里被 Chunk
引用次数并不直接等价于被 import
的次数,而是取决于上游调用者是否被视作 Initial Chunk
或 Async Chunk
处理,例如:
// common.js
export default "common chunk";
// async-module.js
import common from './common'
// entry-a.js
import common from './common'
import('./async-module')
// entry-b.js
import common from './common'
其中,entry-a
、entry-b
分别被视作 Initial Chunk
处理;async-module
被 entry-a
以异步方式引入,因此被视作 Async Chunk
处理。那么对于 common
模块来说,分别被三个不同的 Chunk
引入,此时引用次数为 3
, 配合下面的配置:
// webpack.config.js
module.exports = {
entry: {
entry1: './src/entry-a.js',
entry2: './src/entry-b.js'
},
// ...
optimization: {
splitChunks: {
minChunks: 2,
//...
}
}
};
common
模块命中 optimization.splitChunks.minChunks = 2
规则,因此该模块可能会被单独分包,最终产物:
entry1.js
entry1.js
async-module.js
common.js
4.5 限制分包数量
在 minChunks
基础上,为防止最终产物文件数量过多导致 HTTP
网络请求数剧增,反而降低应用性能,Webpack
还提供了 maxInitialRequest/maxAsyncRequest
配置项,用于限制分包数量:
-
maxInitialRequest
: 用于设置Initial Chunk
最大并行请求数 -
maxAsyncRequests
: 用于设置Async Chunk
最大并行请求数