除了代码分割(Code Spliting)以外,Webpack 还提供了另一种优化代码加载的方式,那就是构建输出分割(Bundle Spliting)。我们通常见到的 Bundle Spliting 的例子是将所有的三方库代码都打包到一个叫 vendor 的 bundle 中。

通过 Bundle Spliting,我们可以实现当应用代码发生变化的时候,浏览器只加载应用代码,应用依赖的三方库代码依然使用缓存。三方库代码发生变化同理。

举个例子来说明,假如在使用 Bundle Spliting 之前,应用构建输出文件为 main.js(100 kb)。使用 Bundle Spliting 之后,应用构建输出为 app.js(10 kb), vender.js(90 kb)。这时候,当应用代码 app.js 发生变化以后,浏览器只需要加载 10 kb 代码就可以了。

为了配合浏览器缓存,我们需要给文件都加上 hash 值,后面章节会详细讨论。

Bundle Splting 通过 optimization.splitChunks.cacheGroups 来实现。

我们在 demo 项目中加入 React.

npm add react react-dom
1

在项目中引入 React。

src/index.js

import "react";
import "react-dom";
...
1
2
3

此时执行 npm run build, 输出结果如下:

⬡ webpack: Build Finished
⬡ webpack: assets by path *.js 127 KiB
    asset main.js 127 KiB [emitted] [minimized] (name: main) 2 related assets
    asset 958.js 183 bytes [compared for emit] [minimized] 1 related asset
  asset main.css 7.94 KiB [compared for emit] (name: main)
  asset index.html 229 bytes [compared for emit]
  Entrypoint main 135 KiB (179 KiB) = main.css 7.94 KiB main.js 127 KiB 1 auxiliary asset
  runtime modules 6.71 KiB 10 modules
  orphan modules 465 bytes [orphan] 2 modules
  code generated modules 133 KiB (javascript) 4.18 MiB (css/mini-extract) [code generated]
    modules by path ./node_modules/ 133 KiB
      modules by path ./node_modules/react/ 6.48 KiB 2 modules
      modules by path ./node_modules/react-dom/ 119 KiB 2 modules
      modules by path ./node_modules/scheduler/ 4.91 KiB
        ./node_modules/scheduler/index.js 198 bytes [built] [code generated]
        ./node_modules/scheduler/cjs/scheduler.production.min.js 4.72 KiB [built] [code generated]
      ./node_modules/object-assign/index.js 2.06 KiB [built] [code generated]
    modules by path ./src/ 633 bytes (javascript) 4.18 MiB (css/mini-extract)
      ./src/index.js + 2 modules 600 bytes [built] [code generated]
      css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[3]!./src/main.css 4.18 MiB [code generated]
      ./src/lazy.js 33 bytes [built] [code generated]
  webpack 5.11.1 compiled successfully in 13865 ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

可以看到,main.js 有 127 kb。现在来优化一下。

添加 vendor bundle

在 Webpack 4 之前,我们使用 CommonsChunkPlugin 来实现 Bundle Spliting。Webpack 4 以后,Webpack 默认支持了 Bundle Spliting,只需要添加一些配置即可。

我们将 node_modules 下的代码都打包到 vendor bundle 中去。

webpack.config.js

const productionConfig = merge([
  ...{ optimization: { splitChunks: { chunks: 'all' } } },
]);
1
2
3

此时,再执行 npm run build, 观察下输出结果。

⬡ webpack: Build Finished
⬡ webpack: assets by status 128 KiB [emitted]
    asset 935.js 125 KiB [emitted] [minimized] (id hint: vendors) 2 related assets
    asset main.js 3.28 KiB [emitted] [minimized] (name: main) 1 related asset
    asset index.html 259 bytes [emitted]
  assets by status 8.12 KiB [compared for emit]
    asset main.css 7.94 KiB [compared for emit] (name: main)
    asset 958.js 183 bytes [compared for emit] [minimized] 1 related asset
  Entrypoint main 136 KiB (181 KiB) = 935.js 125 KiB main.css 7.94 KiB main.js 3.28 KiB 2 auxiliary assets
  runtime modules 7.81 KiB 10 modules
  orphan modules 465 bytes [orphan] 2 modules
  code generated modules 133 KiB (javascript) 4.18 MiB (css/mini-extract) [code generated]
    modules by path ./node_modules/ 133 KiB
      modules by path ./node_modules/react/ 6.48 KiB 2 modules
      modules by path ./node_modules/react-dom/ 119 KiB 2 modules
      modules by path ./node_modules/scheduler/ 4.91 KiB
        ./node_modules/scheduler/index.js 198 bytes [built] [code generated]
        ./node_modules/scheduler/cjs/scheduler.production.min.js 4.72 KiB [built] [code generated]
      ./node_modules/object-assign/index.js 2.06 KiB [built] [code generated]
    modules by path ./src/ 633 bytes (javascript) 4.18 MiB (css/mini-extract)
      ./src/index.js + 2 modules 600 bytes [built] [code generated]
      css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[3]!./src/main.css 4.18 MiB [code generated]
      ./src/lazy.js 33 bytes [built] [code generated]
  webpack 5.11.1 compiled successfully in 13200 ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

main.js 大小变成了 3.28 kb。此时,应用的 bundle 与下图类似。

Bundle Spliting

定制分割输出

上面的配置可以重写如下,我们针对 node_modules 下的文件,使用更加细粒度的控制。

webpack.config.js

const productionConfig = merge([
  ...{
    optimization: {
      splitChunks: {
        cacheGroups: {
          commons: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor',
            chunks: 'initial',
          },
        },
      },
    },
  },
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

initial 对使用了 code spliting 的模块不生效,而 all 可以。

从 Webpack 5 开始,我们可以基于资源类型做更多的自定义配置。

const config = {
  optimization: {
    splitChunks: {
      // css/mini-extra is injected by mini-css-extract-plugin
      minSize: { javascript: 20000, 'css/mini-extra': 10000 },
    },
  },
};
1
2
3
4
5
6
7
8

分割、合并代码块

Webpack 通过如下两个插件来控制最终输出的代码。

  • AggressiveSplittingPlugin 可以分割出更多更小的代码块。这个插件非常适合 HTTP/2.
  • AggressiveMergingPlugin 恰恰相反,会将小的代码块合并成大的代码块。

下面是两个插件的配置样例

const config = {
  plugins: [
    new webpack.optimize.AggressiveSplittingPlugin({
      minSize: 10000,
      maxSize: 30000,
    }),
  ],
};

const config = {
  plugins: [
    new AggressiveMergingPlugin({
      minSizeReduce: 2,
      moveToParents: true,
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

每一个插件都有一个思考平衡点,因为如果代码块分割的越多越小,我们缓存的效果越差,同时请求也越多。

webpack.optimize 包含了 LimitChunkCountPluginMinChunkSizePlugin,提供了针对代码块大小的更进一步的控制。

在入口处的 Bundle Spliting

从 Webpack 5 开始,我们可以在入口处(entry)使用 Bundle Spliting。

const config = {
  entry: {
    app: {
      import: path.join(__dirname, 'src', 'index.js'),
      dependOn: 'vendor',
    },
    vendor: ['react', 'react-dom'],
  },
};
1
2
3
4
5
6
7
8
9

使用了这个配置,我们可以去除 optimization.splitChunks 的配置了,输出效果一样。

注意,在于 webpack-plugin-serve 一同使用的时候,我们需要在 app.import 中加入 webpack-plugin-serve/client

Webpack 中的代码块(Chunk)类型

在上面的例子中,我们使用了多种代码块类型。Webpack 中主要有三种代码块:

  • Entry Chunk, 包含 Webpack 的运行时和需要加载的模块信息表。
  • Normal chunk,不包含 Webpack 运行时,相反,这些代码块通常通过一个包装函数(比如 JSONP)动态加载。
  • Initial chunk,是只在应用初始加载的时候加载的代码块,Initial chunk 是 Normal chunk.

作为用户,我们不需要严格知道每种类型的确切含义,但是要知道 Entry chunk 和 Normal chunk 的区别。

总结

我们可以通过 optimization.splitChunks.cacheGroups 来定义分割形式。在生产构建中,Webpack 会默认使用。

vendor bundle 包含了所有来自于三方包的代码。

Webpack 的一些插件提供了更细粒度的 Bundle Spliting 控制。比如 AggressiveSplittingPluginAggressiveMergingPlugin

Webpack 内部实现依赖了三种代码块类型:Entry chunk, Normal chunk 和 Initial chunk.

关注微信公众号,获取最新推送~

加微信,深入交流~