webpack性能优化——代码切割

默认情况下,webpack会把所有的js代码打包到一个main.js包中,随着项目规模变大,这个时候main包会非常大,导致首次加载会很慢,这个就需要用到代码切割。

代码切割就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载

一般代码切割会从如下几个方面入手:

  • 为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
  • 为 Manifest (Webpack 的 Runtime 代码)单独打包
  • 为不同入口的业务代码打包,也就是代码分割异步加载(同理,也是为了缓存和加载速度)
  • 为异步公共加载的代码打一个的包

默认 Webpack 4 只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks 为 ‘all’,如果需要把runtime也进行分割,则设置runtimeChunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
modules.export = {
optimization: {
splitChunks: {
chunks: 'all'
// 等于如下的配置
// chunks: 'all',
// minSize: 30000,
// maxSize: 0,
// minChunks: 1,
// maxAsyncRequests: 6,
// maxInitialRequests: 4,
// automaticNameDelimiter: '~',
// automaticNameMaxLength: 30,
// cacheGroups: {
// vendors: {
// test: /[\\/]node_modules[\\/]/,
// priority: -10
// },
// default: {
// minChunks: 2,
// priority: -20,
// reuseExistingChunk: true
// }
// }
},
//runtimeChunk: {
//name: "runtime"
//}
}

}

一般情况下,runtime 代码不会单独打包,runtime很小,如果单独打包,单独占用一个http请求,反而增加了成本。

chunks: ‘all’ 默认将根据以下条件自动分割块:

  • 当前模块是公共模块(多处引用)或者模块来自 node_modules
  • 当前模块大小大于 30kb
  • 如果此模块是按需加载,并行请求的最大数量小于等于6(为啥是6?难道是因为浏览器每个域名最大并行请求是6个)
  • 如果此模块在初始页面加载,并行请求的最大数量小于等于4

按需加载

按需加载可以把初始化页面不需要用到的逻辑单独拆分。实现按需加载有两种方式:import 和 require.ensure

下面介绍下 import 的用法

1
2
3
4
5
6
document.getElementsByClassName("async")[0].addEventListener("click",function(){
//异步组件加载
import("./components/async").then(defaults => {
console.log(defaults)
})
})

webpack打包会将async提取出来
image

上面的代码逻辑:当点击async元素时,会动态加载 1.f98c687463dc3d942bd2.js

上面的import是最简单的配置,除此之外下面一些属性:

  • webpackChunkName:chunk 的名称
  • webpackPrefetch:与 link 标签的 prefetch属性有关,将会被浏览器在空闲时间加载,告诉浏览器未来某些导航可能需要资源
  • webpackPreload:告诉浏览器在当前导航期间可能需要该资源

require.ensure() 是 webpack 特有的,已经被 import() 取代。具体使用可以参考这里

自定义拆分规则

在进行不同模块的拆分配置时,有一个重要的概念 cacheGroups 缓存组,它可以继承/覆盖上面 splitChunks 中所有的参数值,除此之外还额外提供了三个配置,分别为:test, priority 和 reuseExistingChunk。

下面是一些常见的参数含义:

  • name:提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index~a.js这样的。
  • test: 表示要过滤 modules,默认为所有的 modules,可匹配模块路径或 chunk 名字,当匹配的是 chunk 名字的时候,其里面的所有 modules 都会选中;
  • chunks:指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all, async, initial,all 代表所有模块,async代表只管异步加载的, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。
  • priority:表示抽取权重,数字越大表示优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件,那么抽取到哪个就由权重最高的说了算;
  • minChunks:控制每个模块什么时候被抽离出去,当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去
  • reuseExistingChunk:表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。

拆分第三方库

根据 node_modules 提取第三方模块,把所有 node_modules 的模块被不同的 chunk 引入超过 1 次的抽取为 vendor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
minChunks: 2,
priority: 2
}
}
}
}

拆分公共业务代码

上面是针对node_modules 中的第三方库进行提取,在开发多页应用,多入口文件的时候,可能会有一些公共的业务代码,在这个基础上,我们还可以再进一步提取公共的业务代码
对于多个入口文件,把所有模块被不同的 chunk 引入超过 1 次的抽取为 common:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//webpack.config.js
optimization: {
splitChunks: {
minSize: 50, //要生成块的最小大小(以字节为单位)
cacheGroups: {
default: {
name: 'common',
chunks: 'all',
minChunks: 2, //模块被引用2次以上的才抽离
priority: 1
},
vendors: { //拆分第三方库
test: /[\\/]node_modules[\\/]/,
name: 'all',
chunks: 'initial',
priority: 2
},
}
}
}

根据priority的优先级,第三方库会被优先提取出来,接着再提出公共的业务代码。

假如项目依赖逻辑如下:

  1. 主入口文件 main.js 按需加载async1 和 async2 文件
  2. async1 和 async2 分别引用公共的组件代码 common 和 第三方库 lodash

那么拆分后的包结构如下所示:

image

除了上面的几种情况,我们还可以独立打包指定模块,具体的业务场景下,具体的拆分逻辑,可以看 SplitChunksPlugin 的文档以及 webpack 4: Code Splitting, chunk graph and the splitChunks optimization 这篇博客。这两篇文章基本罗列了所有可能出现的情况。