【译】SplitChunksPlugin [webpack]

(免责声明:本译文未参考任何其他文章,如有与其他译版雷同部分,纯属巧合)
(声明:中文版权归作者zbc所有,转载请注明出处)
翻译原文地址(webpack官网)


原先,被打包的代码块(和被引入的组件块)都是以一种类似“父—子”模式的关系在 webpack 内部依赖图中关联着。CommonsChunkPlugin 就是用来避免依赖块被重复引用,但是没有其他优化功能。

自从 webpack4 以来,CommonsChunkPlugin 已经被移除,取而代之的是 optimization.splitChunks

默认

SplitChunksPlugin 适用于绝大多数用户。
默认情况下,它只影响按需(on-demand)加载的代码块,因为改变初始代码块会影响运行项目时 HTML 文件包含的 <script /> 标签。

webpack 会基于如下原则自动分割代码:

  • 公用代码块或来自 node_modules 文件夹的组件模块。
  • 打包的代码块大小超过 30k(最小化压缩之前)。
  • 按需加载代码块时,同时发送的请求最大数量不应该超过 5。
  • 页面初始化时,同时发送的请求最大数量不应该超过 3。

当试图完成后两项时,总会生成较大体积的代码块。

配置

webpack 提供一系列配置项给想要更多功能的开发者。

默认配置已被选择为 web 表现层最佳方案,但是这与你项目的最佳策略可能有所不同。当你改变配置项时,你应当测试这些修改确实是为你带来了收益。

optimization.splitChunks

下面这个配置对象体现为 SplitChunksPlugin 的默认配置状态。

//**webpack.config.js**
module.exports = {
  //...
  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
        }
      }
    }
  }
};

splitChunks.automaticNameDelimiter

< string >

webpack 默认会使用原代码块名称来命名新生成的代码块(例如:vendor~main.js)。该配置项可以让你来自定义代码块名称分隔符。

splitChunks.chunks

< function(chunk) | string >

该项指定了哪些块将被优化。如果配置为字符串的话,可选值为“all”, “async”, “initial”。选择 “all” 的功能最为强大,因为这将意味着公共代码块可以同时被异步和非异步块调用。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: 'all'
    }
  }
};

或者你也可以选择传入一个函数来控制。其返回值将决定是否应该包含每一个模块。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks (chunk) {
        // exclude `my-excluded-chunk`
        return chunk.name !== 'my-excluded-chunk';
      }
    }
  }
};

你可以结合 HtmlWebpackPlugin 使用此配置项,它将为你添加所有新生成的代码块。

splitChunks.maxAsyncRequests

< number >

按需加载模块时所允许的最大请求数。

splitChunks.maxInitialRequests

< number >

入口点所允许的最大请求数。

splitChunks.minChunks

< number >

代码拆分前的最小公共块数量。

splitChunks.minSize

< number >

以比特计算,生成代码块的最小体积。

splitChunks.maxSize

< number >

使用 maxSize(无论是全局值 optimization.splitChunks.maxSize,还是每一个缓存组值 optimization.splitChunks.cacheGroups[x].maxSize,还是回调缓存组值 optimization.splitChunks.fallbackCacheGroup.maxSize)会通知 webpack 尝试分割体积大于设定的 maxSize 值的代码块。分割出的块大小将至少为 minSize 值(最大至 maxSize 值)。算法是确定的,并且当模块改变时只会影响本地。所以对于使用长期缓存且不需要记录的需求很有用处。maxSize 只是一个提示,并且当模块大于 maxSize 或分割至小于 minSize 时也完全可以违反之。
一旦一个块已经有了名称,每个其他部分都会以之为衍生获得一个名称,这个名称取决于 optimization.splitChunks.hidePathInfo 的值,并且添加一个从起始块派生来的键名或哈希值。
maxSize 是打算用于配合 HTTP/2 和长效缓存使用的。它增加了请求数以求更好的缓存,它也可以用于快速重构即减小文件体积。

maxSize 的优先级高于 maxInitialRequest/maxAsyncRequests,事实上优先级关系为如下:
maxInitialRequest/maxAsyncRequests < maxSize < minSize.

splitChunks.name

< boolean: true | function (module, chunks, cacheGroupKey) | string >

分割块的名字。如果传入 true 将会自动生成一个基于块组和缓存组键的名称。

传入一个字符串或函数将允许你自定义块名称。指定一个字符串或是一个始终返回同一字符串的函数都会使得 webpack 将所有公共模块融合进一个唯一大代码块。这可能导致一个巨大的初始化下载过程,使得页面加载缓慢。
如果splitChunks.name与入口点名称匹配,则该入口点将被删除。
推荐将splitChunks.name设置为 false ,以避免在打包产品的时候进行不必要的改名动作。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      name (module, chunks, cacheGroupKey) {
        // generate a chunk name...
        return; //...
      }
    }
  }
};

当为不同的拆分块分配相同的名称时,所有打包模块都被放置在一个共享块中,因为这样会导致更多的代码下载,所以不建议这样做。

splitChunks.cacheGroups

cacheGroups 可以继承或覆盖任何来自 splitChunks.* 的设定;但是 "test", "priority","reuseExistingChunk" 只能在本层级上设定。为避免使用任何 cacheGroups 默认设置,把它们统统设置为 false。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false
      }
    }
  }
};

splitChunks.priority

< number >

一个模块可以属于多个缓存组。优化过程总是偏爱优先级更高的缓存组。默认组的优先级为负,以便为自定义组提供相对较高的优先级(自定义组的优先级默认为0)。

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

< boolean >

如果当前块包含一个已经从主打包文件中分离出来的模块,那么它将被重复使用而不是新生成模块。这将影响最终打包生成的块文件名称。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          reuseExistingChunk: true
        }
      }
    }
  }
};

splitChunks.cacheGroups.{cacheGroup}.test

< function (module, chunk) | RegExp | string >

控制那些被缓存组选中的模块。省略它将选择所有模块。它可以匹配模块资源绝对路径或块名称。一旦一个块被匹配成功,当中的所有模块也一同被选中。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test(module, chunks) {
            //...
            return module.type === 'javascript/auto';
          }
        }
      }
    }
  }
};

splitChunks.cacheGroups.{cacheGroup}.filename

< string >

当且仅当一个块是初始块时,允许覆盖文件名。可以获得output.filename的所有字符。

此配置项也可以在全局的 splitChunks.filename 里面设置,但是不推荐这么做,因为当 splitChunks.chunks 没有被设置为 "initial" 时,可能会导致错误,应避免如此做法。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          filename: '[name].bundle.js'
        }
      }
    }
  }
};

splitChunks.cacheGroups.{cacheGroup}.enforce

< boolean: false >

告知 webpack 应该忽略
splitChunks.minSize,splitChunks.minChunks,splitChunks.maxAsyncRequests
splitChunks.maxInitialRequests 的配置项,并且总是为此缓存组创建块。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          enforce: true
        }
      }
    }
  }
};

举例

默认:例1

// index.js

import('./a'); // 动态引入
// a.js
import 'react';

//...

结果:创建了一个包含 react 的独立块。再导入调用的时候,此块与包含“./a” 的原始块并行加载。
为什么:

  • 条件1:此块包含来自 “node_modules” 文件夹的模块。
  • 条件2:react 包大于30k。
  • 条件3:并行的请求调用数为2。
  • 条件4:不影响页面初始加载时的需要。

这之后可以推论出什么?react 可能不会像你的应用代码那样频繁变动。通过将它转移到一个单独的文件包中可以与你的应用代码分别储存(假设你正使用块哈希,记录,缓存控制或其他长效缓存手段)。

默认:例2

// entry.js

// dynamic imports
import('./a');
import('./b');
// a.js
import './helpers'; // helpers is 40kb in size

//...
// b.js
import './helpers';
import './more-helpers'; // more-helpers is also 40kb in size

//...

结果:创建了一个包含 "helpers.js" 及其所有依赖的单独块。导入调用此块时,与原始块并行加载。
为什么:

  • 条件1:此块被两处调用。
  • 条件2:helpers.js 文件大于30k。
  • 条件3:并行的请求调用数为2。
  • 条件4:不影响页面初始加载时的需要。

将 helpers 代码分发至每一个模块会导致此部分代码被下载两次。将之分离出来就可以是这一下载过程只发生一次了。我们付出一次额外请求的代价以折衷此间消耗,这也是为什么我们把分离块的最小判断值设置为30k的原因。

分离块:例1

创建一个包含所有入口点调用的公共代码 common 块。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 2
        }
      }
    }
  }
};

此配置可以扩大你的初始打包文件 bundle.js ,我们推荐您采用动态引入的方式调用那些一开始并不会立刻被用到的代码模块。

分离块:例2

创建一个包含全部来自 node_modules 文件夹的模块代码 vendors 块。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

这可能会产出一个包含所有第三方模块库的大包文件。我们推荐此包应只包含您框架的核心代码和实用代码部分,并且动态加载其余依赖。

分离块:例3

创建一个自定义 custom vendor 包,这个包只包含某些我们用正则式匹配的包。

//**webpack.config.js**
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor',
          chunks: 'all',
        }
      }
    }
  }
};

这将使 react 和 react-dom 合并打包至同一独立块。如果你不确定某一块里面都包含了哪些模块的话,你应该去看一下 Bundle Analysis 去探究细节。

------------------- 结束线 -----------------------

本文翻译结合笔者开发经验,如有错误,欢迎留言指正、探讨。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容