likes
comments
collection
share

Webpack 调优技巧,提升 Web 应用性能与效率

作者站长头像
站长
· 阅读数 11

为什么需要Webpack优化?

优化 webpack 可以改善前端应用程序的性能、加载速度和用户体验。同时提高开发人员的工作效率,并帮助构建出更快、更高效的 Web 应用程序。主要体现在以下几个方面:

  • 打包性能提升
  • 加载性能提升
  • 资源管理
  • 模块化管理
  • 生产环境优化

目标

优化要达成的目标,总结为以下两点:

  1. 提升开发体验
  2. 提升构建产物质量

其中第二点更为重要,直接影响应用的性能、用户体验和流量消耗等方面。

准备

可视化测量工具插件

要达成目标,首先需要一些分析工具帮助我们量化体积、速度等指标,以下这些工具可以作为参考:

  • 打包体积分析工具:webpack-bundle-analyzer
  • 打包耗时分析插件:speed-measure-webpack-plugin
  • 其他
    • progress-bar-webpack-plugin(进度显示)
    • webpack-dashboard(面板显示)
    • friendly-errors-webpack-plugin(更友好的错误输出)

拆分配置文件

在正式的优化配置之前,还需要做一件事,配置文件拆分,这样做主要有两个好处:

  1. 方便维护
  2. 开发和生产构建的区别
    • 优化开发构建以获得更快的开发体验
      1. 优化开发体验
      2. 尽可能减少构建时间
    • 优化生产构建的性能和打包体积
      1. 模块拆包,持久化缓存
      2. 尽可能减少打包文件大小
      3. 代码丑化压缩
      4. 尽可能减少构建时间

优化

代码分割

通过代码分割,拆分模块,去除低效的重复(提出公共大模块),进行合理的冗余(小文件允许重复),从而达到:

  1. 降低包体积
  2. 模块细致化管理,比如:初始加载时只需要下载必要的代码,非必要模块异步加载等
  3. 代码复用和维护性,通过将共享的功能和模块提取到单独的代码块中,可以在不同的页面和应用程序之间实现代码的共享和复用。
  4. 充分利用浏览器的缓存、并发加载等特性,提升应用性能

拆分代码通常来说有几个原则:

  • 不拆分过小的文件,从而减少 http 请求次数
  • 99.99% 不会变动的第三方库,比如 vue 三件套,react 三件套,core-js
  • 大概率不会变动的成熟第三方库,比如 lodashqsaxiosdayjs等,这部分之所以要单独打包是因为和上述基础库相比还是存在一些概率有可能变动,比如 qs@1.1.3 有个 bug 需要升级到 qs@2.0.1 解决
  • 基础类 / 模板类组件,这类组件通常会被多方复用且变动相对业务代码变动不是很频繁,很适合单独打包
  • web workerservice workerwasm 等相关模块,这些模块的特点通常需要检测浏览器支持情况从而决定是否使用,同时需要更多的编译代码,且功能相对独立,适合单独打包从而进行更精细的控制
  • 按需加载的模块
  • webpack 运行时模块(runtimeChunk

注意:代码拆分一般需要配合类库锁版本进行

runtimeChunk

之所以把 runtimeChunk 单独拿出来说一下,是因为它真的很重要,但是大部分人都会用错。 runtimeChunk 可以理解为 webpack 为运行时提供的一些封装,比如要支持异步按需加载需要引入 __webpack_require__.e__webpack_require__.p 等函数,要支持HMRWASMWeb Worker等功能也需要引入对应的封装,这些封装就是 runtimeChunk,也可以叫做 webpack 运行时代码,把这些代码单独打包有以下好处:

  1. 长期缓存:将运行时代码与业务代码分离,可以确保在业务代码发生变化时,运行时代码的 hash 值不会改变。这意味着浏览器可以缓存运行时代码,而只需下载业务代码的更新部分,从而减少用户加载页面的时间和带宽消耗。
  2. 提升构建速度:由于运行时代码相对稳定,将其单独拆分出来后,可以避免在每次构建时都重新生成和打包运行时代码。这样可以提升构建的速度
  3. 代码复用:将运行时代码单独拆分出来,可以在多个入口文件中共享同一份运行时代码。这样可以避免在每个入口文件中重复包含运行时代码,减小打包文件的体积。

注意:runtimeChunk 通常来说比较小,你需要考虑是否需要为了它浪费一次 http 请求,如果不需要可以通过插件把 runtimeChunk 代码直接打进 html 文件,通常来说是一个比较好的做法

除了配置层面,还有一些开发过程中需要注意的情况

精选第三方库

尽量选择一些体积较小的第三方库替代一些较重的库,同时不影响大部分功能,另外选择一些支持 Tree Shaking 的库,可以有效的减少构建产物的体积,下面列举一些示例:

  1. momentjs - dayjs
  2. lodash - lodash-es、ramda、、microdash
  3. Underscore - underscore-es、microbundle、microdash
  4. Vue Lazyload - vue-lazyload-lite

按需加载

一些不是很重要的模块,时效性要求不是很高的模块,大体量模块,可以采用按需加载的方式引入,从而最大限度地优化应用程序的性能和用户体验,同时减少资源浪费,比较典型的是路由的懒加载,总之按需加载有以下好处:

  • 减少初始加载时间
  • 降低初始包大小
  • 避免不必要的网络请求
  • 节省内存消耗

注意,按需加载会增加一些运行时开销,依赖管理困难,需要酌情使用 通常来说的适用场景为:

  • 资源密集型页面
  • 移动端应用

缓存

webpack相关的缓存 一方面是配合拆包、浏览器缓存和 http 缓存实现功能模块的前端缓存,从而提升应用性能, 另一方面是通过对打包结果的缓存,提升 webpack 打包编译的速度,提升开发体验。主要是以下三个方面:

  • 持久化缓存
    • 生成稳定的 hash 值,代码修改实现 hash 值变化最小
    • 分离业务代码和第三方的代码
  • 打包结果缓存
    • 将构建过程中生成的中间文件和缓存结果保存下来,在下次构建时可以直接使用
    • webpack4.0 需要通过一些插件实现,5.0官方支持
  • 插件、loader 配置缓存

压缩

代码压缩

js 压缩

  • terser,参照官方文档
  • esbuild,需要配合 esbuild-webpack-plugin 实现,配置示例如下:
new ESBuildMinifyPlugin({
  target: 'es2015',
  css: true, // Apply minification to CSS assets
  minify: true, // Enable minification
  minifyWhitespace: !isDev, // Minify JS by removing whitespace.
  minifyIdentifiers: !isDev, // Minify JS by shortening identifiers.
  minifySyntax: !isDev, // Minify JS using equivalent but shorter syntax.
  sourcemap: isDev,
  sourcesContent: isDev,
  logLevel: 'info',
})

// 清空已有的使用 `babel-loader` 的规则
config.module.rule('js').uses.clear();
config.module.rule('jsx').uses.clear();
config.module.rule('ts').uses.clear();
config.module.rule('tsx').uses.clear();
config.optimization.minimizers.delete('terser');

// 注入使用 `esbuild-loader` 的新规则
config.module.rule('js')
  .test(/\.m?jsx?$/)
  .exclude.add(path.resolve(__dirname, 'node_modules'))
  .end()
  .use('esbuild-loader')
  .loader('esbuild-loader')
  .options({
    target: 'es2015',
  })
  .end();

css 压缩

css 压缩一般有两步:

  1. mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件
  2. optimize-css-assets-webpack-plugin 插件进行压缩

资源压缩

包括字体、图片等资源的压缩处理,一般是可选操作,通过插件实现,以下是一些常用的插件:

  • image-webpack-loader
  • svgo-loader

文件压缩

一个前端应用通常不会向浏览器直接传输原始文件,而是经过压缩的文件,最耳熟能详的应该是 gzip 压缩,但相信大家都有一个疑问 文件压缩这个工作不是服务器帮我们做的吗?为什么打包工具要做这件事?

回答这个问题,我们得简单了解下服务器压缩的过程,通常来说一个正常的流程如下:

  1. 请求到来时,服务器通过请求头检查支持的压缩格式
  2. 如果支持多种格式还要通过优先级确认要返回的格式类型
  3. 之后查找本地是否有对应格式压缩好的文件缓存,如果有则直接返回,如果没有会现场压缩,之后缓存和返回压缩的文件(也有一些策略是不缓存,每次请求都压缩后再返回)

显而易见,这个过程有两个痛点:

  1. 新文件的第一次请求无法避免压缩的过程
  2. 服务器为了实时性无法按最大级别进行文件压缩

前端进行文件压缩正是为了解决以上两个问题,直接提供压缩好的最高级别的压缩文件,下面是 br 压缩的示例:

new CompressionWebpackPlugin({
  filename: '[path][base].br',
  algorithm: 'brotliCompress',
  test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
  compressionOptions: {
    params: {
      [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
    },
  },
  threshold: 10240,
  minRatio: 0.99,
  //删除原始文件只保留压缩后的文件
  deleteOriginalAssets: false,
})

多线程

多线程打包利用了现代计算机多核处理器的优势,将构建任务分解为多个子任务,并在多个线程中并行执行这些任务。 总之一句话,能开启多线程的就开启

Tree Shaking

更有效的使用 Tree Shaking 需要确保你的代码使用 ES2015 模块语法进行模块导入和导出。这是 Tree Shaking 的基础,因为它依赖于静态解析模块的引用关系。 Tree Shaking 的优化效果取决于代码的结构和使用方式。有时,一些因素(如动态导入、条件导入、第三方库的导入方式等)可能会阻碍 Tree Shaking 的有效工作。 因此,在实际应用中,需要结合具体的项目和代码情况进行评估和调整,以获得最佳的 Tree Shaking效果。

  1. 使用 Pure Annotation:在你的代码中,可以使用 Pure Annotation 来显式声明某个函数是纯函数。纯函数是指在相同的输入下总是返回相同的输出,并且没有副作用。通过使用 Pure Annotation,可以帮助 Webpack 更好地识别和优化这些纯函数,进一步提升 Tree Shaking 效果。
  2. 使用支持 Tree Shaking 的库:并非所有第三方库都能很好地支持 Tree Shaking。在选择使用第三方库时,可以优先考虑那些明确声明支持 Tree Shaking 的库
  3. 配置 WebpackSideEffects:在 Webpack 配置中,可以通过设置 sideEffects 选项来标识哪些模块具有副作用(side effects),以避免不必要的 Tree Shaking。如果你确定某个第三方库或模块没有副作用,可以将其标记为 sideEffects: false,以确保相关代码不被误剔除。
  4. 避免使用全局引入:尽量避免通过全局引入的方式使用第三方库。全局引入会导致整个库的代码被打包到最终的输出文件中,无法进行有效的 Tree Shaking。相反,应该使用按需引入的方式,只引入需要使用的特定模块或功能。

Scope Hoisting

构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。 Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突,开启配置如下:

module.exports = {
    // ...其他配置项
    plugins: [ new webpack.optimize.ModuleConcatenationPlugin() ]
};

注意: 必须是 ES6 的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法

cdn

将一些常用的第三方库(例如 jQueryReactVue 等)通过 CDN 引入,而不是将其打包到最终的构建文件中。这可以减少打包文件的大小,并从静态资源服务器上获取库文件,提高加载速度。

升级 webpack5

  1. 更快的构建速度:Webpack 5 引入了许多优化措施,如持久化缓存、多进程/多线程构建等,从而显著提高了构建速度。通过缓存和增量编译,只需要重新构建修改过的模块,而不是整个项目。
  2. 更小的构建输出体积:Webpack 5 通过引入 Tree Shaking 的默认支持和改进的代码生成算法,可以更好地剔除未使用的代码,从而生成更小的构建输出文件。
  3. 更好的模块系统支持:Webpack 5 提供了对 ES modules 的原生支持,不再需要将模块转换为其他格式,如 CommonJSAMD。这样可以更好地与现代 JavaScript 生态系统中的模块系统进行集成。
  4. 改进的缓存策略:Webpack 5 引入了持久化缓存(Persistent Caching),通过文件内容哈希值来标识模块的版本,从而使缓存更加可靠和可靠。这样,在重复构建时可以从缓存中快速恢复,提高了开发人员的效率。
  5. 更好的 Tree Shaking 支持:Webpack 5 默认启用了深层次的 Tree Shaking,可以更好地识别和剔除未使用的代码。它还引入了 Side Effects Flag(副作用标志)的概念,允许库作者在其代码中提供关于副作用的信息,从而帮助优化器更好地进行 Tree Shaking
  6. 改进的错误处理:Webpack 5 提供了改进的错误和警告信息,使开发人员更容易诊断和调试构建问题。错误消息更加详细和准确,帮助开发人员更快地找到问题所在。
  7. 支持 Module FederationWebpack 5 引入了 Module Federation 的概念,允许将应用程序拆分为多个独立的、可独立部署的子应用。这样可以实现更好的代码共享和独立部署的能力,适用于大型复杂的前端架构。

最后

愿世界上再无 webpack 配置工程师