# SplitChunkPlugin

# 准备

webpack 解析文件时,会生成一张依赖图,里面有许多 chunk 互相依赖 ,webpack 可以将这些 chunk 分割、整合成一个或多个 bundle。这一过程我们可以通过 SplitChunkPlugin 进行调整。

chunk 有两种形式:

  • initial: 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
  • non-initial: 是可以延迟加载的块。可能会出现在使用 动态导入 或者 SplitChunksPlugin 时。

举例:

module.exports = {
  entry: "./src/index.js",
};
1
2
3
// ./index.js
import _ from "lodash";

import("./util.js").then((u) => {
  const blog = _.join(["www", "hxin", "link"], ".");
  u.log(`博客地址:${blog}`);
});

// ./util.js
import $ from "jquery";

export function log(str) {
  console.log($.trim(str));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • initial chunk: ./src/index.jslodash
  • non-initial chunk: ./src/util.jsjquery(除 initial chunk 的所有依赖)

提示

以上内容,有助于你理解以下分割过程和配置信息。

# 默认配置

module.exports = {
  // mode: "production",
  //...
  optimization: {
    splitChunks: {
      chunks: "async",
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: "~",
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
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

以上为 webpack 的默认配置 (mode:production),可翻译如下:

  • 定义 chunk 分割规则 vendors,需要同时满足以下 6 个条件即可分割:
    1. 该 chunk 属于 non-initial chunk
    2. 该 chunk 来自 node_modules
    3. 该 chunk 在打包之前的文件大于 30000 字节
    4. 该 chunk 至少被 1 个文件引入
    5. 该 chunk 分割之后,按需加载最大请求数量不能超过 5
    6. 该 chunk 分割之后,入口文件最大请求数量不能超过 3
  • 定义 chunk 分割规则 default,需要同时满足以下 5 个条件即可分割:
    1. 该 chunk 属于 non-initial chunk
    2. 该 chunk 在打包之前的文件大于 30000 字节
    3. 该 chunk 至少被 2 个文件引入
    4. 该 chunk 分割之后,按需加载最大请求数量不能超过 5
    5. 该 chunk 分割之后,入口文件最大请求数量不能超过 3
    • 注意:如果该 chunk 内部依赖的其他 chunk 被分割出来过且已存在,那么就复用,而不是重新分割出去或打包进来。

优先执行规则 vendors、后执行规则 default。因为规则 vendors 的优先级 (priority) 大于规则 default。打包出来的 bundle 名称自动根据规则名称和入口名称加上~自动组合生成。

# 例子和解析

提示

index.jsutil.js代码和以上准备小节示例一致。

// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "production",
  entry: {
    index: "./src/index.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Webpack example",
      template: "./index.html",
    }),
  ],
  optimization: {
    // 默认配置
    splitChunks: {
      chunks: "async",
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: "~",
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

提示

jqurey 打包前大约是 310kblodash 打包前大约是 550kb,这可能并不准确。

我们可以预先分析打包后的结果,首先分析 initial chunk 和 non-initial chunk,(准备小节)即:

+ initial chunk: ./src/index.js、lodash
+ non-initial chunk: ./src/util.js、jquery
1
2

根据上方可以推断当前可以生成一个 initial bundle 和一个 non-initial bundle。

# 如果 SplitChunkPlugin 不起作用
- initial chunk: ./src/index.js、lodash
- non-initial chunk: ./src/util.js、jquery

+ initial bundle
+ non-initial bundle
1
2
3
4
5
6

再根据分割规则(默认配置小节)推断:

  • initial bundle 忽略分割,因为里面的两 chunk 不符合分割规则 vendors、default 的第一条规则:该 chunk 属于 non-initial chunk,所以就直接将./src/index.jslodash整合成index.bundle.js
  • non-initial bundle 中的 ./src/util.js 不符合分割规则 vendors 的第二条规则:该 chunk 来自 node_modules。也不符合分割规则 default 的第三条规则:该 chunk 至少被 2 个文件引入。当前只被index.js引入。分割规则 vendors、default 都不符合,所以无法分割。
  • non-initial bundle 中的 jquery 符合分割规则 vendors,并且分割规则 vendors 优于 default,所以jquery可以按照规则 vendors 分割出来。

类似下方:

# SplitChunkPlugin 起作用
- initial chunk: ./src/index.js、lodash
- non-initial chunk: ./src/util.js、jquery

- initial chunk: ./src/index.js、lodash
- non-initial chunk: ./src/util.js
- chunk: jquery

+ initial bundle
+ non-initial bundle
+ bundle
1
2
3
4
5
6
7
8
9
10
11

那么打包的结果就是三个 bundle,名字也可以进行分析:

  • initial bundle 名字肯定在入口定义了 entry: { index: ... },名称为 index。
  • bundle 分离出来的模块没有名字,因为我们没定义,所以默认是 number 类型 (递增)
  • non-initial 异步加载模块我们也没有定义,所以也是是 number 类型 (递增)
...
- initial chunk: ./src/index.js、lodash
- non-initial chunk: ./src/util.js
- chunk: jquery

+ initial bundle     -> index.bundle.js
+ bundle             -> id.bundle.js
+ non-initial bundle -> id.bundle.js
1
2
3
4
5
6
7
8
···
          Asset       Size  Chunks             Chunk Names
    1.bundle.js   87.9 KiB       1  [emitted]
    2.bundle.js  195 bytes       2  [emitted]
index.bundle.js   73.4 KiB       0  [emitted]  index
     index.html  238 bytes          [emitted]
···
1
2
3
4
5
6
7

举一反三: 如果想让lodash分离出单独的一个 chunk,可以进行如下设置:

module.exports = {
  // ···
  optimization: {
    splitChunks: {
      chunks: "initial",
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
      },
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这么一来lodash就符合规则 vendors 了,

...
- initial chunk: ./src/index.js、lodash
- non-initial chunk: ./src/util.js、jquery

- initial chunk: ./src/index.js
- chunk: lodash
- non-initial chunk: ./src/util.js jquery

+ initial bundle     -> index.bundle.js
+ bundle             -> vendors~index.bundle.js
+ non-initial bundle -> id.bundle.js
1
2
3
4
5
6
7
8
9
10
11
···
                  Asset       Size  Chunks             Chunk Names
            2.bundle.js   88.1 KiB       2  [emitted]
        index.bundle.js   2.39 KiB       0  [emitted]  index
             index.html  285 bytes          [emitted]
vendors~index.bundle.js   71.3 KiB       1  [emitted]  vendors~index
···
1
2
3
4
5
6
7

# splitChunks.chunks

值:"async""initial""all"function(chunk),默认:"async"

决定选取哪些 chunk 进行优化。

  • "async":对按需加载的模块进行优化
  • "initial":对入口模块进行优化
  • "all":所有(按需加载、入口)模块进行优化,同步与非同步之前还可共享模块
  • function(chunk):传入一个函数,自行控制模块的优化

# splitChunks.automaticNameDelimiter

值:string,默认:~

默认情况下,webpack 将使用 chunk 的来源和名称生成名称(例如:vendors~main.js)。此选项允许您指定用于生成名称的分隔符。

# splitChunks.maxAsyncRequests

值:number,默认:5 (mode:production)、Infinity (mode:development)

按需加载时并行请求的最大数量。

# splitChunks.maxInitialRequests

值:number,默认:3 (mode:production)、Infinity (mode:development)

一个入口最大的并行请求数,入口点处并行请求的最大数量。

# splitChunks.minChunks

值:number,默认:1

模块进行分割前必须共享的块的最小数量。

# splitChunks.minSize

值:number,默认:30000 (mode:production)、10000 (mode:development)

生成一个新 chunk 最小的体积(以字节为单位)。

# splitChunks.maxSize

值:number,默认:0

使用 maxSize(全局:optimization.splitChunks.maxSize、每个缓存组:optimization.splitChunks.cacheGroups[x].maxSize、每个回退缓存组:optimization.splitChunks.fallbackCacheGroup.maxSize)告诉 webpack 尝试将大于 maxSize 的块分割成更小的部分。分割出来的部分的尺寸至少为 minSize(仅次于 maxSize)。该算法是确定性的,对模块的更改只会产生局部影响。因此,当使用长期缓存时,它是可用的,并且不需要记录。maxSize 只是一个提示,当拆分后模块大于 maxSize 或拆分会违反 minSize 时,可以不遵循 maxSize。当块已经有名称时,每个部分将从该名称派生出一个新名称。取决于 optimization.splitChunks.hidePathInfo,它将添加从第一个模块名或它的散列派生的键。maxSize 选项的目是用于 HTTP/2 和长期缓存。它增加了请求数,以便更好地缓存。它还可以用来减小文件大小,以便更快地重新构建。

优先级问题

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

# splitChunks.name

值:truestringfunction (module, chunks, cacheGroupKey),默认:true

分割块的名称。提供 true 将根据 chunk 和 cacheGroup key 自动生成名称。提供字符串或函数将允许您使用自定义名称。如果名称与入口点名称匹配,则将删除入口点。

提示

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

# splitChunks.cacheGroups

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

# splitChunks.cacheGroups.priority

值:number

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

# splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

值: boolean

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

# splitChunks.cacheGroups.{cacheGroup}.test

值:stringRegExpfunction(module,chunk)

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

# splitChunks.cacheGroups.{cacheGroup}.filename

值:string

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

注意

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

# splitChunks.cacheGroups.{cacheGroup}.enforce

值:boolean,默认:false

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