FORMA

产物优化

Webpack 通过代码分割、Tree Shaking、压缩与资源模块等减少体积、改善缓存。原理背景见 工作原理

一、代码分割(Split Chunks)

代码分割的目的是避免重复依赖,将公共代码提取为独立 chunk,充分利用浏览器缓存。

1. SplitChunksPlugin 核心策略

js
// webpack.config.js
optimization: {
  splitChunks: {
    chunks: 'all',          // 对同步和异步模块都进行分割
    minSize: 20000,        // 模块大于 20KB 才单独打包(避免过小 chunk)
    maxSize: 0,            // 不限制最大体积(可设置如 50000)
    cacheGroups: {         // 分组策略
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all',
        priority: 10,      // 优先级高于默认组
      },
      common: {
        minChunks: 2,      // 至少被 2 个 chunk 引用的公共模块
        priority: 5,
        reuseExistingChunk: true,
      },
    },
  },
}
  • chunks: 'all':同时分割同步和异步模块,推荐使用。若设为 'async' 仅分割动态导入模块。
  • minSize:模块体积小于该值的不会单独拆分成 chunk,避免请求过多。
  • maxSize:尝试将大于该值的 chunk 拆分成多个小 chunk,提高缓存命中率(谨慎使用,可能导致请求增多)。
  • cacheGroups:定义提取策略,可以自定义多个分组(如 vendorcommonstyles 等)。

2. 将 node_modules 拆分为单独的 vendor chunk,避免重复依赖

js
cacheGroups: {
  vendor: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendors',
    chunks: 'all',
    priority: 10,
  },
}
  • 原理:所有来自 node_modules 的模块会被合并到 vendors.js 中。
  • 如果多个入口或异步 chunk 引用了相同的第三方库,此配置可避免重复打包。

3. priorityreuseExistingChunk 实战

  • priority:数值越大,该分组越优先匹配。例如 vendorpriority: 10 会优先于默认 default 组的 priority: 0,防止公共模块被错误地归入普通 chunk。
  • reuseExistingChunk:如果当前 chunk 包含的模块已在之前的 chunk 中存在,则直接复用,不再重复打包。减少冗余代码。
js
cacheGroups: {
  default: {
    minChunks: 2,
    priority: -20,
    reuseExistingChunk: true,
  },
}

4. 将多个异步 chunk 中公共的小模块进一步提取(minChunks 配置)

js
cacheGroups: {
  commons: {
    chunks: 'async',        // 只对异步 chunk 生效
    minChunks: 2,           // 至少被 2 个异步 chunk 共用
    name: 'common-async',
    priority: 5,
    reuseExistingChunk: true,
  },
}
  • 对于动态 import() 的页面,如果它们都引用了同一个工具函数(如 lodash 的某个方法),会被提取到 common-async.js,避免重复下载。

二、Tree Shaking

Tree Shaking 删除未使用的代码,前提是必须使用 ES Module(import/export)语法。

1. sideEffects: false 的条件

package.json 中标记 "sideEffects": false,告诉 Webpack 该包的模块都没有副作用(即除了导出外没有执行任何代码),可以安全地删除未被引用的导出。

json
{
  "name": "my-library",
  "sideEffects": false
}

如果某些文件有副作用(如 import './polyfill.js'),则应列出:

json
"sideEffects": ["./polyfill.js", "./style.css"]

2. 对 Babel 编译的要求

Babel 默认将 ESM 转换为 CommonJS,这会破坏 Tree Shaking。需要使用 @babel/preset-env 并设置 modules: false 保留 ESM 结构。

js
// babel.config.js
module.exports = {
  presets: [["@babel/preset-env", { modules: false }]],
};

这样 Webpack 才能分析出哪些导出被使用了。

3. 副作用模块如何标记(如 polyfill、CSS 导入)

  • 对于必须执行一次的模块(如 import '@babel/polyfill'),不能直接删除,因此需要在 package.jsonsideEffects 中明确指定路径。
  • 对于 CSS 文件(import './style.css'),通常也有副作用(样式插入 DOM),同样需要标记,否则可能会被 Tree Shaking 误删。
json
"sideEffects": [
  "*.css",
  "*.scss",
  "./src/polyfill.js"
]

4. usedExports 与 Terser 的协作机制

  • usedExports(Webpack 内置):编译时标记模块中哪些导出被使用了,未被使用的导出会被标记为 /* unused */
  • Terser:在压缩阶段删除这些未使用的代码(包括函数、变量等)。
js
optimization: {
  usedExports: true,   // 开启标记
  minimize: true,
}

两者协作流程:

  1. Webpack 构建模块依赖图,记录每个导出的使用情况。
  2. 在生成代码时,为未使用的导出添加注释。
  3. Terser 读取这些注释,安全地删除无用代码。

三、代码压缩

1. TerserWebpackPlugin 详解

Webpack 5 默认内置 Terser 插件,但也可以手动配置增强。

js
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true, // 多进程并行压缩,提升构建速度
        extractComments: false, // 是否将注释提取到单独文件(如 license)
        terserOptions: {
          compress: {
            drop_console: true, // 生产环境移除 console
            drop_debugger: true,
          },
          output: {
            comments: false, // 移除所有注释
            // keep_fnames: false,   // 保留函数名(对调试有用,但会增加体积)
          },
        },
      }),
    ],
  },
};
  • parallel:利用多核 CPU 加速压缩,建议开启。
  • extractComments:将版权注释提取到 .LICENSE.txt,便于符合开源协议。
  • keep_fnames 权衡:保留函数名有助于堆栈追踪调试,但会增加体积。对于生产环境一般不开启;如果依赖 Function.name 做逻辑判断,则需设为 true

2. CSS 压缩:CssMinimizerWebpackPlugin

替代已经废弃的 optimize-css-assets-webpack-plugin

js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      "...", // 继承默认 JS 压缩(Terser)
      new CssMinimizerPlugin({
        parallel: true,
        minimizerOptions: {
          preset: ["default", { discardComments: { removeAll: true } }],
        },
      }),
    ],
  },
};

注意:CSS 压缩器应放在 minimizer 数组中,与 JS 压缩器并列。

四、资源模块优化

Webpack 5 内置了资源模块(asset modules),取代 file-loaderurl-loaderraw-loader

1. asset 模块类型

类型作用
asset/resource单独输出文件,通过 URL 引用(相当于 file-loader
asset/inline导出资源的 DataURI(相当于 url-loaderlimit 无限)
asset/source导出资源的源代码(相当于 raw-loader
asset自动决定:小于某个阈值时内联为 DataURI,否则输出文件

2. 小资源内嵌的阈值设置

js
module: {
  rules: [
    {
      test: /\.(png|jpg|svg)$/,
      type: 'asset',
      parser: {
        dataUrlCondition: {
          maxSize: 8 * 1024, // 8KB 以下的图片转为 Base64
        },
      },
    },
  ],
}
  • 内联可以减少 HTTP 请求,但会增加 HTML 或 CSS 的体积,需要权衡。

3. 指纹命名([contenthash])的最佳实践

输出文件名使用 [contenthash] 实现长期缓存:

js
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].chunk.js',
},
  • [contenthash] 基于模块内容生成,内容不变则哈希不变,利于浏览器缓存。
  • 对于 CSS 也使用 [contenthash]

4. 预获取/预加载:/* webpackPrefetch: true *//* webpackPreload: true */

  • 预获取(Prefetch):浏览器空闲时提前下载异步 chunk,用于非关键资源(如下一个页面的 JS)。
js
import(/* webpackPrefetch: true */ "./LoginModal.vue");
  • 预加载(Preload):与当前页面同优先级,立即下载,用于当前页面必需但未立即执行的资源。
js
import(/* webpackPreload: true */ "./CriticalChart.vue");

原理:Webpack 会在生成的 HTML 中插入 <link rel="prefetch/preload" href="..."> 标签,告诉浏览器提前加载。需要 HtmlWebpackPlugin 配合。

时机:prefetch 适合懒加载页面的资源;preload 适合当前路由下立即需要但未解析的组件。

注意:过度使用 preload 会增加首屏加载负担。

总结

优化类别关键技术
代码分割SplitChunksPlugin 配置 chunks: 'all'cacheGroupsminChunks
Tree ShakingsideEffects: false、Babel 保留 ESM、usedExports + Terser
代码压缩TerserWebpackPlugin(JS)、CssMinimizerWebpackPlugin(CSS)
资源优化asset 模块、小资源内嵌阈值、[contenthash] 指纹、prefetch/preload

合理运用这些配置,能显著减少产物体积、优化缓存策略、提升页面加载性能。

参考文献

以下链接在编写时均可正常访问:

资料说明
代码分割官方
Tree Shaking官方
Optimization配置项

Series

webpack

2 / 6