三石的webpack.config.js(optimization篇)

2,396 阅读9分钟

依据 mode 不同,执行的优化也不同

splitChunks

webpack已默认集成 splitChunksPlugin 插件,所以不需要单独安装,只需要提供相关配置即可;当然,不配置也可以,有默认配置

条件

webpack 会根据此值将文件拆分成多个 chunk 文件。默认情况下,它只会影响到 按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。 webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30 当尝试满足最后两个条件时,最好使用较大的 chunks
demo1
// index.js
import('./a'); // dynamic import
// a.js
import 'react';
//...

结果:  将创建一个单独的包含 react 的 chunk。在导入调用中,此 chunk 并行加载到包含 ./a 的原始 chunk 中。

原因:

  • 条件1:chunk 包含来自 node_modules 的模块
  • 条件2:react 大于 30kb
  • 条件3:导入调用中的并行请求数为 2
  • 条件4:在初始页面加载时不影响请求
demo2
// 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

//...

结果:  将创建一个单独的 chunk,其中包含 ./helpers 及其所有依赖项。在导入调用时,此 chunk 与原始 chunks 并行加载。

原因:

  • 条件1:chunk 在两个导入调用之间共享
  • 条件2:helpers 大于 30kb
  • 条件3:导入调用中的并行请求数为 2
  • 条件4:在初始页面加载时不影响请求

基本配置

默认参数

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      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,
        },
      },
    },
  },
};

这里只解释部分参数,其他的看官网

  • chunks 表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 allasync 和 initial
    设置为 all 意味着同步和异步引入都可以进行分割
    设置为initial,也会同时打包同步和异步,但是异步引入文件的内部引入不再考虑分割,而是和该异步文件打包在一起 也可以设置成函数,由函数的返回值决定是否包含 chunk
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks(chunk) {
        // exclude `my-excluded-chunk`
        return chunk.name !== 'my-excluded-chunk';
      },
    },
  },
};

可以将此配置与 HtmlWebpackPlugin 结合使用。它将注入所有生成的 vendor chunks。

  • maxAsyncRequests 按需加载时的最大并行请求数。

  • maxInitialRequests 入口点的最大并行请求数。

  • minChunks 要提取的chunks最少被引用多少次

  • minSize 生成 chunk 的最小体积(以 bytes 为单位)。

  • minSizeReduction 生成 chunk 所需的主 chunk(bundle)的最小体积(以字节为单位)缩减。这意味着如果分割成一个 chunk 并没有减少主 chunk(bundle)的给定字节数,它将不会被分割,即使它满足 splitChunks.minSize

为了生成 chunk,splitChunks.minSizeReduction 与 splitChunks.minSize 都需要被满足。

  • enforceSizeThreshold 强制执行拆分的体积阈值,满足此条件后其他限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略
  • maxSize 默认为0,即没有最大体积限制
    使用 maxSize 后 webpack 尝试将大于 maxSize 个字节的 chunk 分割成较小的部分。 这些较小的部分在体积上至少为 minSize(仅次于 maxSize)。

当 chunk 已经有一个名称时,每个部分将获得一个从该名称派生的新名称。 根据 optimization.splitChunks.hidePathInfo 的值,它将添加一个从第一个模块名称或其哈希值派生的密钥。

maxSize 选项旨在与 HTTP/2 和长期缓存一起使用。它增加了请求数量以实现更好的缓存。它还可以用于减小文件大小,以加快二次构建速度

maxSize 比 maxInitialRequest/maxAsyncRequests 具有更高的优先级。实际优先级是 maxInitialRequest/maxAsyncRequests < maxSize < minSize

设置 maxSize 的值会同时设置 maxAsyncSize 和 maxInitialSize 的值。

  • maxAsyncRequests/maxInitialRequest 和maxSize一样,但是 maxAsyncRequests 只影响按需加载,它仅会影响初始加载 chunks

  • cacheGroups 缓存策略,默认设置了分割 node_modules 和公用模块。内部的参数可以覆盖外部的参数。当静态import的模块属于node_modules 目录时,会缓存到到defaultVendors模块下。其余的缓存到default模块

  • cacheGroups.priority 一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority 的缓存组。默认组的优先级为负,以允许自定义组获得更高的优先级

  • cacheGroups.reuseExistingChunk 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块

  • cacheGroups.type 允许按模块类型将模块分配给缓存组。

  • cacheGroups.test 缓存匹配规则,它可以匹配绝对模块资源路径或 chunk 名称,省略它会选择所有模块。

demo

一个优雅的cacheGroup配置:

splitChunks: {
  cacheGroups: {
    vendors: {
      test: /[\/]node_modules[\/]/,
      name: 'vendors',
      minSize: 30000,
      minChunks: 1,
      chunks: 'initial',
      priority: 1 // 该配置项是设置处理的优先级,数值越大越优先处理
    },
    commons: {
      test: /[\/]src[\/]common[\/]/,
      name: 'commons',
      minSize: 30000,
      minChunks: 3,
      chunks: 'initial',
      priority: -1,
      reuseExistingChunk: true // 这个配置允许我们使用已经存在的代码块
    }
  }
}

首先是将node_modules的模块分离出来。异步加载的模块将会继承默认配置,这里我们就不需要二次配置了。 第二点是分离出共享模块,这里其公共代码(或者称为可复用的代码)应该是提取出来放到了src/common中

实例

  1. index.js中,动态引入 a.js
import (/*webpackChunkName:'add'*/'./a.js').then(()=>{
  ...
})
  1. 现在要进行chunk拆分,首先,要对 chunkName 配置
module.exports = {
   ...
   entry: {
        mainname: './src/index.js'
    },
    output:{
        filename:'[name].[contenthash:10].js',
        path:resolve(__dirname,'dist'),
        //对import引入的文件做名字处理,增加contenthash
        chunkFilename:'[name].[contenthash:10]_chunk.js'
    },
}
  1. 然后,增加 splitChunks 配置,怎么配,看自己需求
module.exports = {
    ...
    splitChunks:{
      chunks:"all" // 这里采用其他值亦可以
      // 其他的采用默认值
    }
}

这时候我们打包看看,会发现打包出来两个文件

image.png 解释

  • 'mainame.xxx.js'是因为 entry里key用的是 'mainname',这个值传到了 filename:'[name].[contenthash:10].js',中的[name]
  • 'add.xxx.js'是因为动态引入的时候,/*webpackChunkName:'add'*/这里用了 'add',这个值传到了chunkFilename:'[name].[contenthash:10]_chunk.js'中的[name]
  1. 这时候我们修改a.js内容,重新打包,这时候发现两个文件名都改变了 image.png

其原因如下: 由于 a.js 文件内容改变,所以它的chunk改变(因为使用了chunkContent)。而在'mainame.xxx.js' 文件里,保存了 add 文件的hash值!因此'mainame.xxx.js' 内容也改变了,所以它的chunk名也变了 image.png

很明显,这样是不合理的。webpack想出了一个方法,就是把hash值单独进行打包,这就用到了runtimeChunk

runtimeChunk

作用

先来看看它的功能,就是将当前chunk引入其他chunk的hash单独打包成一个 runtimeChunk 文件

取值

  • true/multiple:针对每个入口打包一个runtime文件
  • single:统一打包一个共享的runtime文件
  • 对象:其 name 属性决定runtimeChunk的名称,可以是函数也可以是字符串
  • 默认值是 false:每个入口 chunk 中直接嵌入 runtime
module.exports = {
  //...
  optimization: {
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },
};

实例

我们接着splitChunks的例子讲!

  1. 增加配置
        //将当前模块记录其他模块的hash单独打包到一个文件runtime
        //打包后,会生成runtime文件
        //如果更改a.js文件,新生成的文件为add.js  和 runtime文件,mainname.js没变
        runtimeChunk:{
            name:entrypoint=>`runtime-${entrypoint.name}`
        }

重新打包,结果如下: image.png

打开看看,发现 'mainame.xxx.js' 里已经没有对 'add.xxx.js'的引用了,在 'runtime-mainame.xxx.js' 里才有

  1. 改变a.js内容,重新打包 image.png 'mainname.xx.js' 并没有改变哦!

chunkIds

告知 webpack 当选择 chunk id 时需要使用哪种算法。
如果为 false,此时 webpack 没有任何内置的算法会被使用,但自定义的算法会由插件提供 常见选项如下:

选项描述
'natural'按使用顺序的数字 id。
'named'对调试更友好的可读的 id。
'deterministic'在不同的编译中不变的短数字 id。有益于长期缓存。在生产模式中会默认开启,webpack5新增
  • 如果环境是开发环境,推荐使用 'named',但当在生产环境中时,推荐使用 'deterministic'
  • 要将 optimization.chunkIds 设置为 false,同时要使用 webpack.ids.DeterministicChunkIdsPlugin
module.exports = {
  //...
  optimization: {
    chunkIds: false,
  },
  plugins: [
    new webpack.ids.DeterministicChunkIdsPlugin({
      maxLength: 5,
    }),
  ],
};

这里顺带讲下 webpack5 新增'deterministic'的意义:

以前,natural 这类是按照顺序来命名文件的,这就会导致,若原先编译的文件是 1 2 和 3,后来 2 相关的代码删掉了,这时候编译的 1 不变,但是 3 就变成 2 了,这样就不能用缓存了。但是用'deterministic'的话,去掉一个 2,编译的依旧是 1 和 3,那么 1 和 3 都可以从缓存中去读取,这样就大大加快了打包速度。

moduleIds

告知 webpack 当选择 module id 时需要使用哪种算法。使用与 chunkIds 一致

minimizer

作用

webpack5 本身默认开启压缩功能,有默认的压缩插件。
但可以通过 minimizer 配置项来使用一个或多个其它压缩插件覆盖默认压缩工具

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        ...
      }),
      new OptimizeCssAssetsPlugin({
        ...
      })
    ],
  },
};

也可以使用函数格式:

module.exports = {
  optimization: {
    minimizer: [
      (compiler) => {
        const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
        new OptimizeCssAssetsPlugin({
          ...
        }).apply(compiler);
      },
    ],
  },
};

常见minimizer

  1. optimize-css-assets-webpack-plugin
    压缩css

  2. terser-webpack-plugin
    webpack的默认压缩js插件

sideEffect

告知 webpack 要不要去识别该项目代码中是否有副作用,从而为Tree-shaking提供更大的压缩空间。
这里的副作用指的是模块执行时除了导出成员之外所做的事情。

开启了 optimization.sideEffects 配置后,webpack在打包时就会先检查需要打包的项目的 package.json 中有没有sideEffects的标识,以此来判断这个模块是不是有副作用。如果这个模块没有副作用,这些没被用到的模块就不会被打包。(这个特性在production模式下会自动开启)

例如:在package.json中配置以下"sideEffects":false 表示整个项目没有副作用,那项目实际出现的一些未使用代码,webpack就不会再打包了
如果该项目中确实有一些副作用,即使没有使用,也不想webpack在打包时删掉,那就以数组的方式提供

  ...
 "sideEffects": ["./src/side-effects.js","*.css"]
  • optimization.sideEffects 取决于optimization.providedExports被设置成启用。这个依赖会有构建时间的损耗,但去掉模块会对性能有正面的影响,因为更少的代码被生成。该优化的效果取决于你的代码库, 可以尝试这个特性以获取一些可能的性能优化
  • 该选项有风险,例如你项目实际是有副作用的,但你判断失误,那么就可能造成打包文件错误

Other

  • nodeEnv 设置 process.env.NODE_ENV,如果不是 false ,则会使用 DefinePlugin。它的默认值取决于 mode
  • removeAvailableModules 将 optimization.removeAvailableModules 设置为 true 后,如果模块已经包含在所有父级模块中,那么 webpack 将从 chunk 中检测出这些模块,或移除这些模块。在当前 production 模式中默认会被开启,下个版本中会被默认禁用,原因是这个开启后会影响 webpack 性能
  • removeEmptyChunks 将 optimization.removeEmptyChunks 设置为 true 后,如果 chunk 为空,那么 webpack 将移出这些chunk。默认为 true
  • emitOnErrors 编译失败时,是否生成资源