深入理解SplitChunksPlugin

2,952 阅读9分钟

我正在参加「掘金·启航计划」

前言

当谈到webpack打包优化时,SplitChunksPlugin插件是最常用也最简便的方法之一。通过简单的配置,实现代码模块的拆分,很有效的解决了代码重复、网络加载冗余内容的问题。尽管大家都配置过SplitChunksPlugin,但很少有人了解各个参数的具体含义和功能。因此,本文将详细介绍SplitChunksPlugin常用配置的具体功能,帮助你更好地了解和使用该插件。

前期准备

首先,我们需要创建一个新项目,准备这样一段代码,文件结构如下:

image.png

我们新建一个src目录,在node_modules文件中添加三个自定义的js,内容非常简单:导出一个字符串来标识模块内容:

// x.js
export default 'x.js';

// y.js
export default 'y.js';

// z.js
export default 'z.js';

然后,们在src目录下直接创建 a、b、c、d、e、f、g文件,其内容也非常简单:

// a.js
import d from './d';
import e from './e';
import x from 'x';
import z from 'z';

export default 'a' + d + e + x + z;
import(/* webpackChunkName: "async-g" */ './g');

// b.js
import d from './d'
import f from './f'
import x from 'x'
import y from 'y'
export default 'b' + d + f + x + y

// c.js
import d from './d';
import f from './f';
import x from 'x';
import z from 'z';
export default 'c' + d + f + x + z;

// d.js
export default 'd';

// e.js
export default 'e';

// f.js 
export default 'f';

// g.js
import f from './f';
export default 'g' + f;

最后,我们创建一个入口文件index.js,代码如下:

import(/* webpackChunkName: "async-a" */ './a')
import(/* webpackChunkName: "async-b" */ './b')
import(/* webpackChunkName: "async-c" */ './c')

上面就是全部的测试代码,为了更方便的看清文件之间的相互依赖关系,我做了一个关系图,可以更方便接下来的理解:

group.png

关闭SplitChunksPlugin

我们创建一个webpack.config.js,做如下配置:

const path = require('path');

const config = {
  mode: 'production',
  entry: {
    main: './src',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    clean: true,
  },
  optimization: {
    minimize: false,
    splitChunks: false,
  },
  context: __dirname,
};

module.exports = config;

我们关闭了splitChunks,此时我们执行yarn build打包后,查看一下打包后的结果:

image.png

此时,我们可以看到,代码分成了5个chunk,在入口文件main.js中,包含了一些webpack的运行时代码用于处理其他chunk的加载逻辑。

x模块被如何引入?

此时,我们来思考一个问题?x模块a、b、c三个chunk同时应用,那么在打包后,x.js的代码被引入了多少次呢?我们在dist文件下搜索x.js的内容,看下结果:

image.png

很明显,x模块的内容被引入了三次,这就得思考两个问题:

  • x模块内容被重复的复制了3次,这看起来是一种资源的浪费,但事实上,webpack只会执行一次,只有一个实例,在运行时每次引用到该模块都返回的是同一个实例

  • 但在文件加载时,会一一发起HTTP请求,假设x模块的内容非常大,此时我们加载的async-a、async-b、async-c将变的非常大,此时加载时间会变得很长,这并不是我们想要的结果

此时,这就引出一个问题:如何解决?

使用SplitChunksPlugin

对于上面提到的问题,我们可以想象这样一种方案:当x模块被多个chunk引用时,如果只发出一个HTTP请求,然后将x模块添加到缓存对象中,这样在其他chunk请求时,x模块将直接从缓存对象中查找

所以,当x模块处于一个单独的chunk时,无论被引用多少次,都只会发起一个HTTP请求

如何决定哪些模块需要被缓存呢,这就需要使用到cacheGroup

默认cacheGroup

SplitChunksPlugin的配置项决定了webpack将如何创建一个新的chunk。而创建一个chunk,就必须要满足一个缓存组的一组规则,例如:

  • 希望来自node_modules中的模块创建成一个chunk
  • 一个模块至少被引用3次才允许创建chunk

类似这样的一组规则,就被称为缓存组,即:cacheGroups

默认缓存组
当没有明确的提及缓存组的情况下,插件将使用如下默认缓存组:


cacheGroups: {
    defaultVendors: {
      test: /[\/]node_modules[\/]/,
      priority: -10,
      reuseExistingChunk: true,
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true,
    },
},

一个default以我们的程序中的任何模块作为目标,而defaultVendors是针对node_modules模块的

同时,defaultVendorspriority较大,表示具有较高的优先级,这在当一个模块存在于多个缓存组中具有重大的作用,它将决定模块属于哪个chunk

此时,我们使用默认的cacheGroup配置:

const path = require('path')

const config = {
  mode: 'production',
  entry: {
    main: './src',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    clean: true,
  },
  optimization: {
    minimize: false,
    /** 
     * 新增 splitChunks
    */ 
    splitChunks: {
      minSize: 0
    }
  },
  context: __dirname,
}

module.exports = config

设置minSize: 0是因为x模块非常小,此时,如果字节数大于0,webpack就会为此创建一个新chunk,执行一次yarn build看下打包结果:

image.png

可以看到多了一些新的chunks: 571.js、616.js、673.js、714.js、934.js

配合前面各个模块的依赖图,我们来解释一下为什么生成这些chunks

z模块 571.js

image.png

  • 属于node_modules文件,满足defaultVendors条件
  • a、c两个chunks引入,满足default条件
  • 但由于defaultVendors 的优先级更高,所以属于defaultVendors

y模块 616.js

image.png

属于node_modules文件,满足defaultVendors条件

d模块 673.js

image.png

  • 不满足defaultVendors条件
  • a、b、c三个chunks引入,满足default条件: minSize: 2

f模块 714.js

image.png

  • 不满足defaultVendors条件
  • b、c两个chunks引入,满足default条件: minSize: 2

x模块 934.js

image.png

  • 属于node_modules文件,满足defaultVendors条件
  • a、b、c三个chunks引入,满足default条件
  • 但由于defaultVendors 的优先级更高,所以属于defaultVendors

以上,是默认配置的效果。


禁用default缓存组

如果我们禁用default缓存组,那么插件将只会考虑defaultVendors缓存组,即只考虑来自node_modules的模块,此时,webpack.config.js

const path = require('path')

const config = {
  mode: 'production',
  entry: {
    main: './src',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    clean: true,
  },
  optimization: {
    minimize: false,
    splitChunks: {
      minSize: 0,
      cacheGroups: {
        /** 
        * 禁用default
        */
        default: false
      }
    }
  },
  context: __dirname,
}

module.exports = config

执行yarn build,看下打包效果:

image.png

这次,我们只生成了3个chunks,分别对应x、y、z三个模块。

探索SplitChunksPlugin其他功能

minChunks

前面我们已经使用过了这个属性,该属性的含义是:拆分前必须共享模块的最小 chunks 数。要想让一些新的chunkSplitChunksPlugin创建,它们必须满足一系列要求,也就是说,这个chunk一定属于某个缓存组

为了使这个更好理解,我们写一个希望满足以下条件的例子:

  • 这个chunks模块必须来自node_modules;
  • 这个模块必须至少出现在3个chunks

此时,webpack应该配置成这个样子:

cacheGroups: {
    default: false,
    defaultVendors: {
        minSize: 0,
        minChunks: 3,
        test: /node_modules/
    }
}

执行一下yarn build看下效果:

image.png

只有934.js,也就是x模块被单独打包出来了,因为这是唯一一个满足上诉条件的模块。

如果我们把minChuns改为2呢?

image.png

此时,打包出了x模块z模块

chunks

我们经常会听到过类似于:”x模块出现在N个不同的chunks中“这样的话。通过chunks属性,我们可以指定这些chunks的类型。

首先,我们需要先介绍一下chunk的相关概念,再次回顾前面的那张关系图:

chunk.png

除了一个chunk可以包含多个模块之外,chunk的概念很难描述。从图中我们可以推断出有两种类型的chunk

  • async-chunk:涉及到async-*的chunk,它们的一个共同点是:都是由import()函数创建的;
  • initial chunk:上图中只有一个chunkinitial chunk。什么是initial chunk?-它和任何其他类型的chunk一样,包含要在应用程序中呈现和使用的模块,但这种类型的chunk还包含大量所谓的运行时代码;这是将所有生成的chunk绑定在一起,以使我们的应用程序正常工作的代码;例如,运行时代码包含加载异步chunk并将它们整合到应用程序中的逻辑;通常,这种类型的chunk可以在entry对象中定义条目时识别出来(例如,{ entry: { main: './index.js' } })

再来看chunks这个参数,它接受4个参数:

  • async:只包含异步chunk
  • initial:只考虑initial chunk
  • all:任何chunk
  • 函数:可以根据特定条件来过滤chunk,只有符合条件的chunk才会被包含进来

chunks: 'all'

值得强调的是chunks选项和minChunks之间的联系。我们使用chunks选项来过滤掉某些chunk,然后SplitChunksPlugin检查剩余chunk的数量是否符合minChunks的要求, 例如:

const path = require('path')

const config = {
  mode: 'production',
  entry: {
    main: './src',
    'a-initial': './src/a',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    clean: true,
  },
  optimization: {
    minimize: false,
    splitChunks: {
      minSize: 0,
      minChunks: 4,
      chunks: 'all',
      cacheGroups: {
        default: false,
      }
    }
  },
  context: __dirname,
}

module.exports = config

如上的webpack配置,我们新增了一个initial chunk:a-initial,因此,x模块会被一个或多个chunk请求,此时我们配置了minChunks为4且chunksall,也就是说,我们希望至少被4个chunks(任何chunk)使用,执行一下yarn build,看下结果:

image.png

可以看到,**x模块(934.js)**被单独打包成一个chunk

chunks: 'async'

如果将chunks修改为async后,再次打包:

image.png

x模块没有被单独打包成一个chunk,这是因为没有一个模块出现在4个异步chunks中。

chunks: 'initial'

当我们将chunks修改为initial时,执行yarn build看下结果:

image.png

我们发现并没有生成新的chunk。这是因为这个webpack配置的含义是:

  • 模块必须来自node_modules(使用了默认缓存组中的defaultVendors)
  • 一个模块至少在4个initial chunk中出现

在我们的项目中,只有一个initial chunk需要这样的模块,即:a-initial,而main.js只通过import函数引用模块。所以,我们需要把minChunks: 4改为minChunks: 1, 看下打包后的结果:

image.png

image.png 此时,a-initial中引用到的x模块z模块,被打包到了一个新的chunk中。

总结

本篇文章我们介绍了webpack中的SplitChunksPlugin插件最常用参数的使用方式和功能,详细解释了代码分割和优化策略的相关概念。通过本文的学习,读者可以更好地理解如何利用SplitChunksPlugin插件优化webpack打包的性能,同时也能够更加灵活地运用该插件解决项目中的实际问题。希望本文能够为您带来实际帮助,欢迎探讨和交流。

感谢阅读🙏