webpack构建优化思路

173 阅读9分钟

前言

本文为性能优化系列的其中一篇文章,可以点击查看其他文章。

在现代前端开发中,构建速度和代码体积对应用的性能有着至关重要的影响。Webpack 作为最流行的模块打包工具之一,为我们提供了丰富的优化手段。通过合理的配置和插件使用,我们可以显著减少打包时间和输出文件的大小,从而提升应用的加载速度和用户体验。本文将深入探讨一些实用的 Webpack 优化策略,帮助你在开发中最大化地提升项目性能。

1. Mode

是的没错,mode也是有优化的空间的。这个mode的含义不仅限于webpack配置的mode,更在于我们应该清晰的知道我们引入的loader、plugins,哪些是只需作用于生产环境,哪些是只作用于开发环境,将他们区分开来,避免影响效率。通常的做法就是创建多个配置文件,在运行指令时通过--config参数指定配置文件。例如创建webpack.common.jswebpack.development.jswebpack.production.js分别对应通用配置、开发环境配置、生产环境配置。

下面列出一些常见的只用于生产环境的插件

  • TerserPlugin: 用于压缩和优化 JavaScript 代码,减少文件体积
  • MiniCssExtractPlugin: 将 CSS 提取到单独的文件中,避免 CSS 代码被内联到 JavaScript 文件中
  • CompressionWebpackPlugin: 使用 gzip 或 Brotli 压缩输出的静态文件,以减少文件传输体积
  • BundleAnalyzerPlugin: 分析打包后的文件,生成报告帮助开发者了解打包体积和模块依赖。

如果你发现你的webpack配置中有这些插件作用于开发环境,请在开发环境中移除,会给你带来很多惊喜噢。

2. output

2.1 filename

output的主要优化点在于output.filenameoutput.chunkFilename,开发环境中我们推荐使用[name].js进行文件命名,比[name].[contenthash].js有更好的性能,更适合热更新等频繁更新的场景,且webpack自带的热更新会帮我们处理浏览器的缓存问题,绝大部分情况下都不需要担心我们改动了浏览器依旧使用缓存。

而对于生产环境,则建议使用[name].[contenthash].js,避免更改了内容而浏览器依旧使用了缓存导致更新无效。

2.2 多入口共享依赖

默认情况下,每个入口 chunk 保存了全部其用的模块(modules)。使用 dependOn 选项你可以与另一个入口 chunk 共享模块。详细请看Dependencies

3. plugins

3.1 SpeedMeasurePlugin

SpeedMeasurePlugin 是一个测量打印每个loader、plugins耗时的插件,是在做性能优化时的好助手。 这个插件平时可以不用开启,在引入新loader、plugins时或者在做性能优化专项时启动就好。

3.2 WebpackBundleAnalyzer

WebpackBundleAnalyzer 是可以对生成物的体积大小通过treemap的形式直观的展示出来,方便做分析与优化,不推荐在平时日常开发时使用,理由同上。

3.3 EslintWebpackPlugin

包括但不限于EslintWebpackPlugin等各种lint的webpack plugins都推荐从配置中移除,因为通常我们的IDE已经有针对各种lint开发出对应的插件辅助我们日常的开发,使用husky配合git的钩子以及CICD配置自动化流水线做检查等手段足以保证我们的代码质量,无需放到webpack中影响性能。

3.4 TerserPlugin

webpack默认的压缩插件,支持移除无用代码、压缩混淆、移除注释等功能,减少生成物的体积。只建议用于生产环境。

3.5 MiniCssExtractPlugin

将 CSS 提取到单独的文件中,避免在 JS 文件中内联 CSS。这样可以更好地利用浏览器的缓存机制,并减少主文件的大小。只建议用于生成环境。

3.6 CompressionWebpackPlugin

可以生成 Gzip 或 Brotli 格式的压缩文件,这些文件在 Web 服务器上提供给客户端时,可以显著减少传输的字节数。只建议用于生成环境。

3.7 SplitChunksPlugin

Webpack 内置的插件,用于将代码拆分成更小的块,以实现按需加载。它可以将公共模块(如依赖库)抽取出来,减少重复加载。一般只建议用于生成环境。

3.8 DllPlugin和DllReferencePlugin

webpack内置的插件,通过提前打包一些不经常更改的库文件(如 React、Vue、Lodash 等),可以加速主项目的构建时间。这些插件让你能够预先打包这些库,并在主项目中直接引用它们。

3.9 IgnorePlugin

webpack内置的插件,用于在打包过程中忽略指定的模块或文件。这可以帮助你减少打包体积、优化构建速度,尤其是在你知道某些依赖或模块在特定环境下不会被使用时。例如你确定你无需使用某些包的国际化包,那么可以配置忽略它们。

3.10 PurgeCSSPlugin

PurgeCSSPlugin 可以分析你的项目,移除未使用的 CSS,从而减少 CSS 文件的大小。PurgeCSSPlugin

3.11 ProgressPlugin

这个插件用于报告构建进度,把这个插件移除可以减少构建时间。需要你自行权衡是否使用这个插件。ProgressPlugin

4. module

4.1 noParse

防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中 不应该含有 import, require, define 的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。

4.2 loader相关
  • 利用includes/excludes/test,合理配置loader生效范围,避免对所有文件进行处理
  • 对于比较耗时的loader(例如babel-loader),使用thread-loader启用多线程并行处理, cache-loader缓存编译结果。文档在这thread-loadercache-loader。如果同时使用的话,cache-loader放第一,thread-loader放第二,最后再放耗时的loader,并且需要注意的是你必须要确定是耗时的loader,因为多开线程和缓存结果也是消耗资源的。
  • 识别哪些loader应该适用于生产环境,哪些适用于开发环境,这里举个例子MiniCssExtractPlugin
4.3 babel-loader

babel-loader也有缓存选项,如果不想额外启用cache-loader,可以开启

{
  test: /\.js$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true,
      },
    },
  ],
  exclude: /node_modules/,
}
4.4 ts-loader

如果你并非一个严格的纯typescript项目,不妨试试使用babel-loader + fork-ts-checker-webpack-plugin替代ts-loader,babel-loader 来编译 TypeScript 代码,同时通过 fork-ts-checker-webpack-plugin 在独立的进程中进行类型检查。这样可以在确保类型检查的同时,提升构建速度。

4.5 rule.type

Rule.type是webpack5提供的资源模块,它允许使用资源文件而无需额外配置loader。由于是内置的,所以免除了引入额外的loader,如file-loaderurl-loaderraw-loader。并且可能会有有更好的性能。推荐使用。

5. devtool

devtool控制生成source-map。需要考虑的不仅是性能,还有安全性和调试体验。所以参考这个表devtool和评估你自己的项目,选择最合适自己项目的source-map。如果还是拿不准,那么推荐开发环境用eval-source-map,能提供最准确的调试信息,但是相对应的构建最慢。生产环境默认false,不生成source-map,避免泄露源码,但是相应的出错了调试困难。

6. resolve

  • 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
  • 如果不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false。
  • 如果使用自定义解析插件规则,并且没有指定上下文,可以设置 resolve.cacheWithContext: false。

7. Optimization

默认的Optimization配置已经能满足大部分中小型项目的优化需求了,比如

  • optimization.minimizeoptimization.minimizer默认启用,在生产模式下会自动压缩 JavaScript 和 CSS 文件
  • optimization.splitChunks默认配置会自动分割代码,提取公共模块到独立的 chunks
  • optimization.moduleIdsoptimization.chunkIds 默认使用 natural,在大多数情况下,这已经足够好,但可以根据需求选择更稳定的 deterministic 策略。

对于大型复杂项目就不太好给出标准答案,这里只简单提几点

  • minimizer可以配置使用TerserPlugin, 移除注释、debugger、console.log等不该出现在线上的代码,减少体积,通过parallel选项启用并行,提升构建速度
  • SplitChunks 针对自己的项目做更加细致的拆分和控制并行请求数量,可以配合WebpackBundleAnalyzer以及lighthouse做相对应的测试。

8. Externals

我们的项目可能在生产模式下可能引用部分CDN,那么我们不会希望在打包过程中把这些依赖打包进来。那么我们可以通过Externals来将依赖排除出去。

9. cache

cache默认是开发环境中开启memory模式,生产环境默认关闭。我们可以通过配置cache.type来切换memoryfilesystem模式

他们之间的区别是

  • memory: 缓存保存在内存中,所以速度更快,webpack进程结束后缓存消失,不会对webpack重启构成影响。适合频繁变更构建时使用。
  • filesystem: 保存在磁盘中,所以构建速度稍有影响,但是因为是持久化存储,在webpack重启时依然生效。

不管生产环境还是开发环境都推荐使用filesystem, 在二次启动时加速效果极其明显,对比memory, 热更新场景下的影响不显著。

10. tree-shaking

10.1 usedExports

是用于标记哪些 ES6 模块的导出在最终的代码中被使用了。Webpack 会根据这些标记来移除未使用的导出,从而减少打包后的代码量,生产模式默认启用。由于 ES6 模块的静态结构特点,usedExports 只能对使用静态分析的方法标记未使用的导出,对于动态导入或复杂的使用情况可能不完全有效。可以使用/*#__PURE__*/注释来标记函数无副作用,如无调用可以清除。

10.2 sideEffects

sideEffects 是用于指定哪些模块或文件具有副作用的配置选项。副作用是指模块或文件在导入时会产生影响(如修改全局变量、执行代码等),即使没有使用其中的导出。根据你的项目情况,合理的配置sideEffects,不正确的 sideEffects 配置可能导致必要的代码被错误移除,影响应用的正常运行。因此,在设置 sideEffects: false 时,需要确保模块或文件确实没有副作用。

总结

核心其实就几点

  1. 加速构建速度,手段无非就是缓存、多线程、更优的工具(版本)、少做无用功(includes/excludes)
  2. 降低生成物的体积
  3. 生成物拥有更优的缓存效果

掌握优化的思路,把握好核心要素,才能更好的针对实际项目情况做出更优的优化措施。