# webpack配置optimization

用于优化输出的文件,通过在module.exports对象中添加属性optimization配置。

从 webpack 4 开始,会根据选择的 mode 来执行不同的优化, 不过所有的优化还是可以手动配置和重写。

# optimization.minimizer

optimization.minimizer:允许通过提供一个或多个定制过的 TerserPlugin 实例,覆盖默认压缩工具(minimizer)。

在optimization对象中还具有一个minimizer属性,这个属性和plugins属性功能相同,都是用来设置plugin的。

而两者的区别在于:optimization.minimizer会受到optimization.minimize属性的管理

optimization.minimizer属性会受到optimization.minimize属性的控制:如果optimization.minimize属性值为false,那么就不加载设置在optimization.minimizer属性中的plugin;也就是optimization.minimize是控制optimization.minimizer属性的开关。

 optimization: {
    // 压缩的操作
    minimizer: [
      // 如果配置了css压缩,可能相当于不再使用webpack默认配置,js就不会再压缩了,需要调用TerserWebpackPlugin
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin()
    ],
    splitChunks: {
      chunks: "all",
    },
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
  },

# optimization重复代码优化

如果多入口文件中都引用了同一份代码,不希望这份代码被打包到两个文件中,导致代码重复,体积更大。

需要提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // 单入口
  // entry: './src/main.js',
  // 多入口
  entry: {
    main: "./src/main.js",
    app: "./src/app.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "js/[name].js",
    clean: true,
  },
  mode: "production",
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // minSize: 20000, // 分割代码最小的大小
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
      // 修改配置
      cacheGroups: {
        // 组,哪些模块要打包到一个组
        // defaultVendors: { // 组名
        //   test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
        //   priority: -10, // 权重(越大越高)
        //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
        // },
        default: {
          // 其他没有写的配置会使用上面的默认值
          minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

# import动态加载

在wbepack5中高版本,没有配置也会将动态引入的文件单独打包

  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all",
      // 其他都用默认值
    }
  }

# optimization.runtimeChunk 减少引用之间打包

a引用了b,结果b内容改了,a打包的文件hash值也会改变,但是如果生成一个runtime文件去管理这些hash值,那么就可以不需要重新打包a文件了!

注意使用的不是hash而是contenthash,

object string boolean

将 optimization.runtimeChunk 设置为 true 或 'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk。此配置的别名如下:

module.exports = {
  //...
   output: {
    path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
    // [contenthash:8]使用contenthash,取8位长度
    filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
    chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
    assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
    clean: true,
  },
  optimization: {
    runtimeChunk: {
      name: (entrypoint) => `runtime~${entrypoint.name}`,
    },
    // runtimeChunk:true
  },
};

# Scope Hoisting

默认情况下,经过 Webpack 打包后的模块资源会被组织成一个个函数形式,例如:

// common.js
export default "common";

// index.js
import common from './common';
console.log(common);

上例最终会被打包出形如下面结构的产物:

"./src/common.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
     const __WEBPACK_DEFAULT_EXPORT__ = ("common");
     __webpack_require__.d(__webpack_exports__, {
      /* harmony export */
      "default": () => (__WEBPACK_DEFAULT_EXPORT__)
      /* harmony export */
    });
  }),
"./src/index.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js");
      console.log(_common__WEBPACK_IMPORTED_MODULE_0__)
  })

这种结构存在两个影响到运行性能的问题:

  • 重复的函数模板代码会增大产物体积,消耗更多网络流量
  • 函数的出栈入栈需要创建、销毁作用域空间,影响运行性能

针对这些问题,自 Webpack 3 开始引入 Scope Hoisting 功能,本质上就是将符合条件的多个模块合并到同一个函数空间内,减少函数声明的模板代码与运行时频繁出入栈操作,从而打包出「体积更小」、「运行性能」更好的包。例如上述示例经过 Scope Hoisting 优化后,生成代码:

((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    ;// CONCATENATED MODULE: ./src/common.js
    /* harmony default export */ const common = ("common");
    
    ;// CONCATENATED MODULE: ./src/index.js
    console.log(common);
})

Webpack 提供了三种方法开启 Scope Hoisting 功能的方法:

  • 开启 Production 模式
  • 使用 optimization.concatenateModules 配置项
  • 直接使用 ModuleConcatenationPlugin 插件
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
    // 方法1: 将 `mode` 设置为 production,即可开启
    mode: "production",
    // 方法2: 将 `optimization.concatenateModules` 设置为 true
    optimization: {
        concatenateModules: true,
        usedExports: true,
        providedExports: true,
    },
    // 方法3: 直接使用 `ModuleConcatenationPlugin` 插件
    plugins: [new ModuleConcatenationPlugin()]
};

# 模块合并规则

开启 Scope Hoisting 后,Webpack 会将尽可能多的模块合并到同一个函数作用域下,但合并功能一方面依赖于 ESM 静态分析能力;一方面需要确保合并操作不会造成代码冗余。因此开发者需要注意 Scope Hoisting 会在以下场景下失效:

  1. 非 ESM 模块 对于 AMD、CMD 一类的模块,由于模块导入导出内容的动态性,Webpack 无法确保模块合并后不会对原有的代码语义产生副作用,导致 Scope Hoisting 失效,例如:
// common.js
module.exports = 'common';

// index.js
import common from './common';

上例中,由于 common.js 使用 CommonJS 导入模块内容,Scope Hoisting 失效,两个模块无法合并。

这一问题在导入 NPM 包尤其常见,由于大部分框架都会自行打包后再上传到 NPM,并且默认导出的是兼容性更佳的 CommonJS 模块方案,因而无法使用 Scope Hoisting 功能,此时可通过 mainFileds 属性尝试引入框架的 ESM 版本:

module.exports = {
  resolve: {
    // 优先使用 jsnext:main 中指向的 ES6 模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  },
};
  1. 模块被多个 Chunk 引用 如果一个模块被多个 Chunk 同时引用,为避免重复打包,Scope Hoisting 同样会失效,例如:
// common.js
export default "common"

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

// index.js 
import common from './common';
import("./async");

上例中,入口 index.js 以异步引用方式导入 async.js 模块,同时 async.js 与 index.js 都依赖于 common.js 模块,根据 Chunk 的运行规则, async.js 会被处理为单独的 Chunk ,这就意味着 common.js 模块同时被 index.js 对应的 Initial Chunk 与 async.js 对应的 Async Chunk 引用,此时 Scope Hoisting 失效,common.js 无法被合并入任一 Chunk,而是作为生成为单独的作用域,最终打包结果:

 "./src/common.js":
  (() => {
    var __WEBPACK_DEFAULT_EXPORT__ = ("common");
  }),
 "./src/index.js":
  (() => {
    var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js");
    __webpack_require__.e( /*! import() */ "src_async_js").then(__webpack_require__.bind(__webpack_require__, /*! ./async */ "./src/async.js"));
  }),  
最后更新: 5/1/2023, 9:10:04 AM