Webpack SplitChunk 分包

理解 chunk

Webpack可以看成一个模块打包机器,我们编写的文件对于Webpack来说都是模块(module),我们可以在Webpack中配置对应模块的处理方式。

chunkWebpack在打包过程中处理一些module的合集。配置entry入口文件,入口文件再依赖其他的modulewebpack会根据这些引用关系逐个打包模块,将module打包在一起,也就形成了 chunk

Untitled

chunk是构建流程中的中间产物,会根据一些默认配置决定那些模块合并打包;也可以通过 splitChunks自定义打包策略。

默认三种分包处理

  • Initial Chunk entry 模块及相应子模块打包成 Initial Chunk
  • Async Chunk 通过import('./xx')等语句导入的异步模块及相应子模块组成的 Async Chunk
  • Runtime Chunk 运行时代码抽离成 Runtime Chunk,可通过 entry.runtime 配置项实现

默认情况

默认情况下,它只会影响到按需加载的chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

Webpack 将根据以下条件自动拆分chunks

  • 新的chunk可以被共享,或者模块来自于  node_modules  文件夹
  • 新的chunk体积大于 20kb(在进行 min+gz 之前的体积)
  • 当加载Async Chunk时,并行请求的最大数量不得超过 30
  • 当加载Initial Chunk时,并发请求的最大数量不得超过 30

当尝试满足最后两个条件时,最好使用较大的chunks

Webpack spiltChunks 默认的config如下:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      // 表示新分离出的chunk必须大于等于minSize,默认为20000,约20kb。
      minSize: 20000,
      // 用于确保在分割 chunk 之后,剩余部分的大小不小于某个阈值
      minRemainingSize: 0,
      // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
      minChunks: 1,
      // 表示按需加载文件时,并行请求的最大数目。默认为5。
      maxAsyncRequests: 30,
      // 表示加载入口文件时,并行请求的最大数目。默认为3。
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\/]node_modules[\/]/,
          // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
          priority: -10,
          // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

模块均为同步导入

import { Home } from './component/home';
import { sum } from 'lodash';

const App = () => {
  return (
    <div>
      <Home />
    </div>
  );
};

export default App;

Untitled

打包出来只有一个main文件,也就是我们的入口文件

lodash 动态导入

import Home from './component/home';
import(/* webpackChunkName: "async_lodash" */ 'lodash');

const App = () => {
  return (
    <div>
      <Home />
    </div>
  );
};

export default App;

Untitled

lodash被单独打包出来了,因为异步加载的模块会被单独打包

React 组件按需加载

// Home.jsx
import { merge } from 'lodash';

const Home = <div>home,{merge({ a: 1 }, { b: 1 })}</div>;

// App.jsx
const Home = React.lazy(() => import('./component/home'));

const App = () => {
  return (
    <div>
      <Home />
    </div>
  );
};

export default App;

Untitled

被打包成了三个文件,由于Home组件时按需加载的因此单独打包。因为在Home中引入了 lodash,因此lodash是异步加载的,也会被单独打包。

lodash 按需加载

import sum from 'lodash/sum';

const Home = <div>home,{sum(1, 2)}</div>;

export default Home;

Untitled

因为lodash/sum的文件大小小于 20kb,不会被单独打包,会和Home组件打包在一个文件中,因此只有两个文件

共享模块的打包

// Home
export default function () {
    return (
        <div>
            <Button2>1111</Button2>
        </div>
    );
}

// Home2
export default function () {
    return (
        <div>
            <Button2>2222</Button2>
        </div>
    );
}

// Button
export default function (props) {
    return (
        <>
						 {/* 放一张超过 20kb 的图片,使 button 的大小超过 20kb */}
            <button>{props.children}</button>
        </>
    );
}

分析一下依赖视图如下:

image.png

打包出来结果如下:

Untitled

看到一共有四个文件,分别是mianHomeHome2button,由于button满足上诉的默认分包情况,因此会被单独拎出来。

入口模块和异步模块的公共库

// App
import { sum } from 'lodash';
const Home = React.lazy(() => import('./component/home'));

const App = () => {
  return (
    <div>
      {sum(1, 2)}
      <Home />
    </div>
  );
};

// Home
import { sum } from 'lodash';

export default function () {
  return <div>{sum(1, 2)}</div>;
}

Untitled

这种情况下,lodash 会打包在入口文件中,Home 组件中不在打包 lodash 会和入口文件用一份

配置项

目前SplitChunksPlugin已经在Webpack内部支持了,我们引入的时候不需要在添加一来了,可以直接修改 optimization.splitChunks 或者通过plugins的方式修改optimize.SplitChunksPlugin({})

分包范围

SplitChunks 在默认的情况下只会对 Async Chunk 生效,提供了chunks字段来调整范围

  • 字符串'all' :对 Initial Chunk 与 Async Chunk 都生效,建议优先使用该值;
  • 字符串'initial' :只对 Initial Chunk 生效;
  • 字符串'async' :只对 Async Chunk 生效;
  • 函数(chunk) => boolean :该函数返回true时生效;

使用频率

可以根据modulechunk引用的次数来决定是否分包。通过minChuks来配置最小引用次数,可以将频繁使用的模块打包成独立文件,减少重复代码。

⚠️ 注意:这里的引用次数不代表为import的次数,而是取决于上有引用者是否被作为了Initial Chunk或者Async Chunk

minChunks默认值为 1

// common.js
export default 'common chunk';

// async-module.js
import common from './common';

// entry-a.js
import common from './common';
import('./async-module');

// entry-b.js
import common from './common';

entry-a/entry-bInitial Chunk处理;async-module作为Async Chunk处理,那么 common被 3 个不同的chunk依赖;被引用次数为 3 次

module.exports = {
  entry: {
    entry1: './src/entry-a.js',
    entry2: './src/entry-b.js',
  },
  // ...
  optimization: {
    splitChunks: {
      minChunks: 2,
      //...
    },
  },
};

根据上述配置,最终可能被分成四个包,entry-a/entry-b/async-module/common

限制分包数量

如果我们分包的数量很多,会导致http网络请求比较多,反而降低性能。提供了maxAsyncRequests/maxInitialRequests

  • maxAsyncRequests 按需加载时候最大的并行请求数,默认为 30
  • maxInitialRequests 入口文件的最大并行请求数,默认为 30

image.png

module.exports = {
  mode: 'development',
  devtool: false,
  entry: {
    entry1: './src/entry-a.js',
    entry2: './src/entry-b.js',
    entry3: './src/entry-c.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    splitChunks: {
      minChunks: 2,
      chunks: 'all',
      minSize: 1,
    },
  },
};

针对于上述的配置,最后打包结果如下:

Untitled

三个入口文件和对应两个common文件都满足打包条件

那在此时对于entry-b来说,请求entry-b的同时需要请求common-1/common-2,并行数量为 3,我们将maxInitialRequests设置为 2,打包结果如下

Untitled

会发现common-1这个文件,打包进入了entry-a/entry-b文件中

限制分包体积

提供了一系列大小判断的条件来处理chunk,可以在chunk过小时取消打包;在chunk过大时,再次尝试拆解。

  • minSize: 超过这个尺寸的chunk才会正式被分包;
  • maxSize: 超过这个尺寸的chunk会尝试进一步拆分出更小的chunk
  • maxAsyncSize: 与maxSize功能类似,但只对异步引入的模块生效;
  • maxInitialSize: 与maxSize类似,但只对entry配置的入口模块生效;

根据这几个规则,SplitChunksPlugin的流程如下:

  • 将命中minChunks规则的module统一抽到一个额外的chunk对象;
  • 判断该chunk是否满足maxInitialRequests阈值,若满足则进行下一步;
  • 判断该chunk资源的体积是否大于上述配置项minSize声明的下限阈值;
    • 如果体积小于minSize则取消这次分包,对应的module依然会被合并入原来的chunk
    • 如果chunk体积大于minSize则判断是否超过maxSizemaxAsyncSizemaxInitialSize声明的上限阈值,如果超过则尝试将该chunk继续分割成更小的部分

image.png

对于上面这种依赖关系来说,假设配置minChunks大于 2,minInitialRequests大于 2,如果common包的体积大于miniSize分包成功,common作为独立的chunk,否则会分别合并进入 3 个chunk

缓存组 cacheGroups

上面讲到的都是对哪些module做分包处理,可以使用cacheGroups为不同的文件组设置不同的规则。

如果匹配上cacheGroups的分组会优先使用改组下的minChunks/minSize等条件配置。

cacheGroups也有独有的配置项:

  • test: 接受正则表达式、函数、字符串,所有符合 test 判断的都会被分到改组
  • name: 当前分组的名字
  • priority: 数字型,用于设置改分组的优先级,若命中多个缓存组,优先被分到 priority 更大的组
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
        },
      },
    },
  },
};

上述配置的话,能够把所有的第三方库都帮我们打包到命名为 vendor 的分组中

Untitled

对于默认配置中的cacheGroups来说,能帮助我们:

  • 将所有node_modules中的资源单独打包到vendors-xxx-xx.js命名的产物
  • 对引用次数大于等于 2 的模块,也就是被多个chunk引用的模块,单独打包
cacheGroups: {
  defaultVendors: {
    test: /[\/]node_modules[\/]/,
    priority: -10,
    reuseExistingChunk: true,
  },
  default: {
    minChunks: 2,
    priority: -20,
    reuseExistingChunk: true,
  },
},

总结

该篇文章主要讲解了splitChunk的对应的分包策略,相关配置项的具体使用。