谈一谈Webpack 的SplitChunks

18,608 阅读6分钟

webpack官网中,有这么一句话,我不是很认同:

开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

可以说,性能优化最重要的部分就是懂得如何分包了。因此,就来谈谈webpack中的分包。

首先我们需要了解一个概念。

什么是chunks

一个常考的面试题是 module、chunk、bundle是什么。对于初学者,这个是很迷惑的。我们首先要知道webpack一个简单的理解:

Webpack: 构建你的assets。左侧的资源通过webpack后,都能输出web浏览器能识别的资源! 归因于node构建了一个世界,让这个世界所有非js资源,都在js世界下的规则下处理:

css: 本质是style下的一个字符串,webpack处理css文本后,如果要直接作为style标签插入,再使用style-loader,如果要拿出来,则通过MiniCssExtractPlugin插件进行处理。 less: 本质最终要转化成css,因此通过less-loader,转化为css,再重复上面的操作。 typescript: 浏览器是没法直接识别的,通过ts-loader。来转化为浏览器能识别的js。

以上这些,都是为了做一件事:

以上图webpack为划分点,我们就可以区分module和bundle。即:左边是资源就是moudle,右边的资源就是bundle。

一个ts文件、图片、less、pug等都是一个module,而打包后的产物,总的称呼就是bundle。

那么chunk是什么呢?

对于打包产物bundle, 有些情况下,我们觉得太大了。 为了优化性能,比如快速打开首屏,利用缓存等,我们需要对bundle进行以下拆分,对于拆分出来的东西,我们叫它chunk。

我们试着打包一下!

默认配置

现在我们创建src/index.js 和 src/a.js

index.jsa.js
import lodash from "lodash";,import { a } from "./a";,console.log(lodash, a);export const a = "i am aaaaaa";,console.log(a);,

目录结构:

webpack.config.js 配置和打包结果为:

webpack.config.js
{, mode: "production",, entry: {, main: "./src/index.js",, },, output: {, path: path.resolve(__dirname, "dist"),, filename: "[name].js", , clean: true,, },,}

默认的,webpack没有进行分包,全部都打在一起了。

一些配置字段

Webpack 的分包主要在optimization.splitChunks属性,现在我们来尝试使用不同的配置字段来看看效果。

optimization.splitChunks.chunks

Chunks 有三个提供的值,分别是 async、initial、all

async

此值是默认的chunks值,也就是说,我们的第一次打包实际上就是实行了async,该值的意思是:对于动态加载的模块,默认配置会将该模块单独打包。使用以下语法进行动态加载(还有其他写法):

import('lodash')

修改index.js,然后运行build命令,为了文件更直观,我们将optimization.chunkIds的值设置为named

index.js
import { a } from "./a";,import('lodash').then(lodash => {, const res = lodash.default.add(3,4), console.log(a, res);,})

可以看到lodash被分出一个单独的包了。

以下是webpack对于默认配置的说法:

webpack 将根据以下条件自动拆分 chunks: 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积) 当按需加载 chunks 时,并行请求的最大数量小于或等于 30 当加载初始化页面时,并发请求的最大数量小于或等于 30 当尝试满足最后两个条件时,最好使用较大的 chunks。

进行实验后,发现并不准确,比如两个入口引入lodash,lodash并未被抽出来

index.jsother.js
import lodash from "lodash";,import { a } from "./a";,console.log(lodash, a);import lodash from "lodash";,,console.log("lodash", lodash);

配置与打包

配置打包
{, entry: {, main: "./src/index.js",, other: "./src/other.js",, },, optimization: {, chunkIds: "named",, },,}
默认配置只会抽出动态加载的模块,通常情况下,不是立即需要的包,可以考虑动态加载,比如导出excel的包,echarts,monaco-editor等。

initial

当chunk为initial或者为alll时,webpack打包遵循以下配置(取名default):

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'initial',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

尽管你的配置可能是这样:

{, entry: {, main: "./src/index.js",, other: "./src/other.js",, },, optimization: {, chunkIds: "named",, splitChunks: {, chunks: "initial",, },, },,}

只有当chunks 不为async时,webpack打包的默认配置才会是default配置

新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积) 当按需加载 chunks 时,并行请求的最大数量小于或等于 30 当加载初始化页面时,并发请求的最大数量小于或等于 30

all

当chunks值为all时,基本跟initial值相同,我们来实验一下它的不同点;

我们将在other.js对lodash动态引用:

import('lodash').then(lodash => {
    const res = lodash.default.add(3,4)
    console.log(res);
})

然后分别使用chunks为all 和initial的值,看看效果:

allinitial

可以看到,对于两个入口文件引用lodash, 如果一个是正常引入,一个是动态引入,initial会打包成两份,而all的话,只会有一份,因此,通常情况下,all的优于initial的。

optimization.splitChunks的其他默认配置

{
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
}

minSize:此配置是指,将要被分包的chunks,如果压缩前体积不足20k,将不会被拆包。 minChunks:某个chunks被多次引用,如果这个引用次数小于某个值,将不会被拆包。 ... 以上条件满足一个将会被分包。 enforceSizeThreshold:如果某个chunks的大小超过了50k,以上限制将不会生效。

optimization.splitChunks. cacheGroups

cacheGroups有两个默认缓存策略,也就是chunks为all和initail时的默认配置:

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

defaultVendors 会将源代码中所有引入node_modules的文件打包成为一个大的chunks。 default 则是对于多入口引入的相同模块超过两次后,进行拆包操作,需要注意的是,我们通常操作的单页面应用,默认只有一个入口文件,如果有如下代码:

// index.js
import lodash from "lodash";
import { a } from "./a";
import { b } from "./b";
console.log(lodash, a, b);

// a.js
export const a = "i am aaaaaa";
console.log(a);
import "./c";

// b.js
export const b = "i am bbbbbbbbb";
console.log(b);
import "./c";

// c.js
console.log("ccccccccc");

a.js 和 b.js 共同引用了c.js。此时a、b、c都从属于index.js入口,虽然c.js被引用了两次,但c.js并不会分成单独的包,如果要将c.js单独打包,考虑动态加载。

通常,我们只需要默认的配置即可满足大部分需求,有的时候我们可能想单独抽出react相关代码,那么这需要以下配置。

react: {
  name: "ReactAbout",
  test: /react/,
  priority: 1,
},

打包效果

// other.js

import('lodash').then(lodash => {
    const res = lodash.default.add(3,4)
    console.log(res);
})

import('./style/a.css')
import('./style/b.css')
import('./style/c.css')
{
    test: /\.css$/,
    use: ["style-loader", "css-loader"],
}

css

css也是性能优化的一部分,有一种方式是通过style- loader将css以style标签的形式插入到文档,这种方式正常引用无法进行分包。但可以通过动态引入的方式分包。

另外有一个MiniCssExtractPlugin插件进行css的分包。此插件默认为每个入口单独抽出css,也可以进行cacheGroups的配置,满足条件时,会将多个入口的css打包在一起。

css: {
  name: "css",
  test: /\.css$/,
  minChunks: 1,
  enforce: true,
}