头图

极致编译速度,一文搞定webpack5升级

本文作者:xiongxiao01

在尝试升级 webpack5 之前,建议大家尽量先把官方文档通读一遍,可以少走很多弯路,本文是在结合具体业务场景后,对官方文档的归纳和补充。

背景

music-musician-web-node 是音乐人场景的核心应用,覆盖的页面多,代码量大,在使用 webpack4 的情况下,本地开发的编译效率一直不是很好,以笔者使用的 MacBook Pro M1 为例,音乐人 + 版权总共 45 个页面,启动一次全量构建需要花费 140s 左右 (已经加了诸如 happypack 之类的并行打包策略)
image.png

增量构建一般是 4-5s,但是一旦修改引用次数比较多的比如公用的 ts 定义,则增量编译时间一度能达到 80s 以上
image.png

因此,随着业务需求的增多,为了提升需求交付效率,解决开发体验的问题,就变得非常急迫。在解决开发体验方面,升级 webpack 是最快捷且直接能带来巨大收益的手段。

webpack5 带来的提升

webpack4 到 webpack5 毕竟有很多的 break change,为了让大家能有升级的热情,那么还是必须先展示升级带来的提升。我们就以创作者中心的数据来说明。
在上面的背景里面我们已经讲了原先开发时 webpack 启动和增量构建的时间分别是140s和平均4-5s,这里我们直接列出升级后的成果

启动时间降低

webpack 启动时间缩短到55s左右,减少了近60%
image.png

增量时间降低

增量构建时间非常夸张,直接到了1s左右,即使是原先可能会导致构建时间达到 80s 以上的 ts 定义修改,也被降低到3s左右,平均减少80% 以上
image.png

生产包构建时间

之前在本地大约是 240s 左右,升级后为 200s。
image.png
注意:一定要升级 terser-webpack-plugin 到最新版,否则生产包构建时间相较于 webpack4 可能还会降低。

包体积变化

因为创作者中心的项目页面非常多,不太好比较 webpack4 和 webpack5 打包结果体积的变化,但是从官方优化策略(这个优化策略还导致了很多 break change,这个在下文会细讲)和很多实践结果看,对包体积都是有优化的。

升级前置准备

从这里开始,我们就正式着手 webpack 的升级。在正式升级前,需要先做一些前置工作,这些工作可以帮我们规避之后升级过程中一些奇怪的问题。

  • 将 webpack 升级到 v4 的最新版本:如果你当前使用的版本就是 v4,升级应该是无痛的,但如果 webpack 是 v3 甚至更早的版本,请参考官方文档先升级到 v4
  • 将 webpack-cli 升级到最新版
  • 将使用的 loader 和 plugin 都升级到兼容 v4 的最新版本:这里要留意每个 plugin 和 Loader 对于 webpack 版本的要求,因为有些 loader/plugin 的最新版是只兼容 webpack5 的,我们这里还暂时不需要升级到这一步

因为每个项目所使用的 loader 和 plugin 有一些差异,因此这里实在不方便列出所有对应的版本,只能以笔者自身的项目为例,把常见的 plugin 和 loader webpack4 对应的最新版本列出来,其他的请根据自己项目的情况,到 npm 或者 github 查找 readme 或者翻阅 release 记录,几乎所有符合标准的工具库都会给出

  • terser-webpack-plugin: webpack4 请升级到 4.x, webpack5 升级到最新
  • babel-loader: 无论 v4 或者 v5, 升级到 7.x 或者 8.x 都可以
  • extract-text-webpack-plugin: v4 请升级到最新版,v5 之后用 mini-css-extract-plugin 进行替换
  • optimize-css-assets-webpack-plugin: v4 升级到最新版,v5 用 css-minimizer-webpack-plugin 替换
  • ts-loader: v4 升级到 8.x,v5 升级最新版
  • less-loader: 7.x 为兼容 webapck4 的最后版本
  • sass-loader: 10.x 为兼容 webpack4 的最后版本
  • css-loader: 5.x 为兼容 webpack4 的最后版本
  • postcss-loader: 4.x 为兼容 webpack4 的最后版本
  • ...

修复 warning 和 errors

做完上述的前置准备以后,不出意外,运行编译流程,会有一些报警甚至是错误,请修复这些问题。

更改 v4 版本过时的写法

v4 版本就在告警的过时写法在 v5 版本一般都会直接抛弃,所以我们需要对照官方给出的指引,进行更改,否则升级 webpack5 后 webpack 是无法启动的

  • optimization.hashedModuleIds: true → optimization.moduleIds: 'hashed'
  • optimization.namedChunks: true → optimization.chunkIds: 'named'
  • optimization.namedModules: true → optimization.moduleIds: 'named'
  • NamedModulesPlugin → optimization.moduleIds: 'named'
  • NamedChunksPlugin → optimization.chunkIds: 'named'
  • HashedModuleIdsPlugin → optimization.moduleIds: 'hashed'
  • optimization.noEmitOnErrors: false → optimization.emitOnErrors: true
  • optimization.occurrenceOrder: true → optimization: { chunkIds: 'total-size', moduleIds: 'size' }
  • optimization.splitChunks.cacheGroups.vendors → optimization.splitChunks.cacheGroups.defaultVendors
  • optimization.splitChunks.cacheGroups.test(module, chunks) → optimization.splitChunks.cacheGroups.test(module, { chunkGraph, moduleGraph })
  • Compilation.entries → Compilation.entryDependencies
  • serve → serve is removed in favor of DevServer
  • Rule.query (deprecated since v3) → Rule.options/UseEntry.options

测试 webpack5 的兼容性

这一步非常重要,webpack5 相比于 webpack4 一个非常显著的差异,就是 webpack5 为了优化 bundle size,不再默认支持 node polyfill,所以一旦一些 node 与 browser 环境公用的包内部使用了 node 的全局变量,则会引发非常严重的 runtime 阶段的报错,为了尽量提前发现问题,我们需要在去掉 node 全局变量的情况下先测试下代码的运行情况
注意:这里的写法仅用于 webpack4 测试兼容性阶段,在升级 webpack5 之后记得去掉

module.exports = {
  // ...
  node: {
    Buffer: false,
    process: false,
  },
};

在补充完上述代码后,先运行自己代码看看,是否会遇到页面报错,注意,这些报错是 runtime 阶段的,并不是编译阶段就会出现的,所以得实际运行页面才能看出来。
如果你的代码直接或者间接依赖了 node 的 process 和 Buffer 变量,那么你肯定会遇到下面的报错

    ReferenceError: Buffer is not defined

或者

    ReferenceError: process is not defined

在实际的业务场景中,我们一个工程往往有非常多的页面,且每个页面状态繁多,无法简单的看出是否有 runtime 报错,所以我建议读者直接就当做代码中确实就是有 node 全局变量的引用,然后在升级 webpack5 的时候手动进行 polyfill,这样可以将问题概率降到最低,具体怎么做 polyfill,接下来就会讲到。

开始 webpack5 升级

在上述前置准备完成后,我们可以开始进行正式的升级了。

安装最新 webpack 版本

nenpm install -D webpack@latest

更改配置

  • 移除optimization.moduleIdsoptimization.chunkIds的配置,webpack5 优化了 chunkId 和 moduleId 的默认生成策略,直接用默认的会更高效
  • 使用[hash]占位符的地方都可以换成[contenthash],后者会更高效
  • IgnorePlugin在入参为 regexp 的时候,写法改成new IgnorePlugin({ resourceRegExp: /regExp/ })
  • 对于 webpack4 中如node.fs: 'empty'的写法,在 webpack5 中,node 属性只支持三个字段,global,__dirname,__filename参考,所以除这三个属性外的 node 属性,需要放到resolve.fallback中,因此node.fs: 'empty'需要改为resolve.fallback.fs: false
  • url-loader,raw-loader,file-loader建议直接用Assets Module替换,虽然不换也暂时不影响,但是在后面的版本,这三个 loader 可能被移除

针对optimization.splitChunks,在 v5 版本尽量参考下列配置

  • splitChunks 推荐使用默认配置,或者直接optimization.splitChunks: { chunks: 'all' }
  • 如果原先有这种写法optimization.splitChunks.cacheGroups: { default: false, vendors: false },需要改成optimization.splitChunks.cacheGroups: { default: false, defaultVendors: false }

需要更正或者省略的写法

/* webpackChunkName: '...' */

之前的代码逻辑中,我们会使用这种注释写法给 code split 的 chunk 命名,但是在 v5 版本,可以不用这么做,当 mode 为 development 的时候,webpack 会自动以文件名来命名 chunk

引用 Json 的变化

原先的这种写法

import { version } from './package.json';
console.log(version);

需要改成

import pkg from './package.json';
console.log(pkg.version);

处理升级过程中的错误

一般来说,在升级过程中,会遇到三类问题

schema 错误

webpack 的配置语法错误,这种错误很好解决,在报错信息中 webpack 会详细标明错误原因和改进方法,按照提示进行修复即可。

编译时报错

这类问题也不难解决,最常见的就是因为 webpack5 升级后不再做 node 的 polyfill,会报module not found,这里列出官方给出的 webpack4 默认的 polyfill,大家根据业务场景的报错,自行下载对应的 npm 模块,然后添加配置即可(不需要全部加上,根据报错内容按需添加)

module.exports = {
  //...
  resolve: {
    fallback: {
      assert: require.resolve('assert'),
      buffer: require.resolve('buffer'),
      console: require.resolve('console-browserify'),
      constants: require.resolve('constants-browserify'),
      crypto: require.resolve('crypto-browserify'),
      domain: require.resolve('domain-browser'),
      events: require.resolve('events'),
      http: require.resolve('stream-http'),
      https: require.resolve('https-browserify'),
      os: require.resolve('os-browserify/browser'),
      path: require.resolve('path-browserify'),
      punycode: require.resolve('punycode'),
      process: require.resolve('process/browser'),
      querystring: require.resolve('querystring-es3'),
      stream: require.resolve('stream-browserify'),
      string_decoder: require.resolve('string_decoder'),
      sys: require.resolve('util'),
      timers: require.resolve('timers-browserify'),
      tty: require.resolve('tty-browserify'),
      url: require.resolve('url'),
      util: require.resolve('util'),
      vm: require.resolve('vm-browserify'),
      zlib: require.resolve('browserify-zlib'),
    },
  },
};

还有一种可能的情况是之前能引用的 npm 包,在升级以后引用不到了,需要变更写法,这种比较少见,但是创作者中心这里遇到过。应用依赖的二方包依赖了 uuid 这个库,之前的写法是

import uuidv4 from 'uuid/dist/v4';

// ...

然而升级之后,这种写法在编译时就报错了,需要改成

import { v4 as uuidv4 } from 'uuid';

这种错误就很难预料,但好在是编译时的报错,还是有迹可循的,遇到后再 google 其实问题也不大。

runtime 阶段报错

以创作者中心升级的经验来看,会有两种引发 runtime 阶段报错的原因

node 全局变量 polyfill

最常见的也是 node polyfill 的丢失,上面也提到过,node 与 browser 公用的一些包中会使用一些 node 的全局变量 (process,Buffer),这些变量是直接使用的,并没有 require,因此在编译阶段不会暴露。
为了解决这类问题,需要这么配置

{
plugins: [
        new webpack.ProvidePlugin({
            Buffer: ['buffer', 'Buffer'],
            process: 'process/browser',
        }),
    ],
}

使用 ProvidePlugin 将变量注入到全局,这里注意要下载对应的 buffer 和 process/browser 的包,这里我推荐大家都将 buffer 和 process 的 polyfill 加上,以防万一

import 后变量丢失

这个也是升级过程中实际遇到过的。
部分 npm 包导出的写法并不是特别规范,在 webpack4 的版本会自动被兼容掉,但是 webpack5 因为默认支持 tree shaking,对于 npm 包的导出写法会更严格,因此存在一种情况,即

import watermark from 'watermark-dom'

这种写法在升级后,引用到的变量是 undefined,同时也没有警告和编译错误
需要改成

import { watermark } from 'watermark-dom';

这种 runtime 阶段的报错处理起来就非常棘手,关键在于难以预料,无法防范,上面只是列举了创作者中心升级过程中的 runtime 报错,不同的业务场景可能还会遇到其他的报错,所以千万不要有侥幸心理,唯一稳妥的办法是将升级的影响范围告知 qa,让 qa 做一次完整回归。

部分 loader 的优化

去掉 cache 相关的 loader 和 plugin

webpack5 内部对于 cache 的优化和利用已经非常好了,不需要再使用 webpack4 阶段用来优化缓存的 loader 和 plugin 来提升性能,只会引发未知的问题,建议全部去掉。

用 thread-loader 替换 happypack

happypack 的作者本人已经不再维护该模块,而 thread-loader 是官方推出的多线程编译优化方案,性能据说要好不少,推荐在升级完成后进行替换。

使用 css-minimizer-webpack-plugin 替换 optimize-css-assets-webpack-plugin

该 loader 用于 css 代码压缩,官方给的建议是升级后进行替换
image.png

用 @babel/preset-typescript 替换 ts-loader

推荐尝试,不仅仅在配置上简化很多,一套 babel 配置吃遍所有 js,同时在性能方面,个人体验后感觉确实会快不少。
不过 @babel/preset-typescript 因为没有再走 tsc,所以编译时不会再像 ts-loader 那样将编译错误暴露出来,这里需要单独装一个插件 [fork-ts-checker-webpack-plugin
](https://github.com/TypeStrong...) 来进行编译过程中的语法校验

后记

webpack5 升级最难解决的问题还是 runtime 阶段的异常报错,这些报错都是跟具体的业务场景挂钩,无法枚举。本文的目的也是以当前业务场景的升级经验,将一些问题处理思路提供出来供大家参考。
以上就是本次创作者中心升级 webpack 的总结,希望对大家有所帮助。

本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。我们常年招收各类技术岗位,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe (at) corp.netease.com!

网易云音乐技术团队
网易云音乐大前端技术团队专栏

网易云音乐技术团队

3.2k 声望
3.3k 粉丝
0 条评论
推荐阅读
网易云音乐数据全链路基线治理实践
摘 要:在大数据开发领域,大家都会被一个问题困扰:调度任务延迟,然后被老板、被业务“灵魂拷问”。本文将从问题挑战、目标衡量、行动方案、成果展示、后续规划五个方面展开,详述网易云音乐在全链路基线治理的实...

云音乐技术团队阅读 220

封面图
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木140阅读 11.8k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 5.9k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.1k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(>^ω^&lt...

XboxYan41阅读 2.8k评论 14

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图

网易云音乐技术团队

3.2k 声望
3.3k 粉丝
宣传栏