就这几个小操作,我把项目性能优化了几十倍!

14,070 阅读14分钟

前言

说到性能优化,大家肯定能想到很多内容。比如说如何修改 Webpack 配置来达到构建提速以及优化产物的需求;比如说如何对页面进行性能优化等等。

其实性能优化还存在很多可以玩的地方,今天笔者就来聊一些大家不常关注的地方,从开发到 CI 的构建阶段以及最后部署上线这几个链路。

构建阶段

构建阶段分为两个部分:

  • 本地开发
  • CI 构建

这两部分各自侧重的点并不相同。前者更加关注构建速度,对于产物没有什么要求,毕竟本地开发时候构建是一件很频繁的事情,速度慢就意味着降低开发效率;后者更关注产物,比如说文件体积、数量等等指标,当然了构建速度也不能说完全不关心,但多是在保证产物的前提下再去提升构建速度。

接下来笔者会就这两部分来分别聊聊我们可以做的性能优化。

本地开发

以 Webpack 为例,本地开发构建这部分是我们日常工作中最常接触到的,如果能带来一定的提速,那么感知还是挺强的。

这部分主要分为两个阶段:

  • 启动项目,比如说 yarn start
  • 修改保存代码触发热更新

第一阶段相对来说频率低点,速度慢点在大部分情况下还是可以忍受的。

但是第二阶段是我们前端开发每天都会进行数十次甚至上百次的高配操作,并且对于一些中大型项目来说,在这部分花的时间一次可能就需要 10 秒多。假设老王每天修改 100 次代码,那么在这部分每天都得等待上 15 分钟甚至更多。

那么我们有办法去提效这部分内容么?答案是有的,应该很多读者都早已了解到这类产品了:Vite

Vite 属于 NoBundle 方案,也可以称之为 UnBundle、Bundleless,反正都是同一个玩意。同类竞品还有 snowpackwmr 等,当然名气都不如前者大。

如果你已经使用过 Vite,那么应该对它秒级启动以及热更新有了映像,相比单纯的 Webpack 提升巨大。

截屏2021-06-06下午10.05.06

上图出自该文章,项目有 15 万行代码,很大型的一个项目了,大家可以发现在启动以及热更新上真的是有数十倍的提升,这对于开发体验来说真的流畅很多。另外笔者之前也参与过公司内部 NoBundle 方案的研发,得出的数据也是很可观的。

那么为什么 Vite 能带来这样的体验的呢,到底是如何实现这样的效果的?笔者这里就粗略的来聊聊。

首先来聊聊 Webpack 构建,基于 Webpack4 以及中大型项目来说。当我们执行 yarn start 以后,Webpack 会开始全量打包,构建出依赖图后把所有内容都打成几个文件,这个构建依赖图以及打包的过程会花掉我们数分钟。

当触发热更新的时候,Webpack 也需要找出这一条依赖链路并再次对链路进行打包,因此这个过程耗时也不少。当然如果你用上了 Webpack5 的话,基于持久化缓存应该能提速不少。

基于打包器的开发服务器

但是对于 Vite 来说就不需要这样干了。

首先 Vite 使用到了 ESBuild 来预构建依赖,这是一个用 Go 写的特别牛逼的构建器,效率相比 JS 来说快了数十倍甚至百倍,Vite 需要利用这个来处理模块以及构建 ESM 环境。

另外就是得益于 ESM 按需加载的特性,我们无需启动项目的时候构建依赖图以及打包文件,而是浏览器请求什么文件才编译什么文件(比如说编译 TS、插入热更新代码等),依托于这个特性我们能很快的跑起来项目。

最后当用户触发热更新的时候,Vite 也无需像 Webpack 那样做,而是找出最小的依赖路径(一般来说就是修改的那个文件),然后修改下文件的 hash 下发给浏览器失效之前的缓存即可。

基于上面的一些特性以及 ESBuild,Vite 基本不会因为代码量变大而造成速度有明显拖慢,但是对于 Webpack 来说,项目体积很明显会拖慢构建速度。

基于 ESM 的开发服务器

读者看到这里,可能觉得这玩意确实牛逼,摩拳擦掌准备干上一番。但是,这里要给各位泼个冷水。根据我们内部的 Nobundle 使用结果以及笔者与多位在大厂做过这个方案的朋友交流的结果来看,接入业务成本巨大,虽然提效不错,但是以接入的成本来看可能投入产出比就不是那么可观了。目前也没有一套很好的接入方案,很可能在不同的项目里会踩到不同的坑,最大的原因还是来自于 ESM 环境。

但是笔者认为 Nobundle 以后一定会成为本地开发构建的主流,因为开发体验实在太顺滑了。

另外很可能读者会问 Vite 是否会取缔 Webpack 这类问题。笔者认为这两个东西算不上是竞品,Webpack 能适用于复杂、需要定制化的场景,这点 Vite 是做不到的(未来可能也不会去做),Vite 主要是为了改善开发阶段的体验,顶多在开发阶段 Vite 能顶替掉 Webpack 的工作。

以下笔者列了点资料,大家有兴趣可以了解下:

最后大家如果有兴趣在业务里做迁移的话,一定要多看看市面上迁移相关的文章,能帮助大家在踩坑的时候快速解决问题。

CI

在 CI 中构建大致分为三个环节:

  1. 安装依赖
  2. 代码质量保障
  3. 构建

前两个环节涉及到的内容不多,笔者这里就快速带过。

安装依赖

依赖安装还是挺耗时的,我们可以通过以下几点来加速这个环节:

  • 源必须切换到淘宝源或者自己的私有源
  • 缓存 node_modules 必须整上
  • 有条件可以试试 yarn2

这里稍微聊一下 yarn2 这个事情。升级到 yarn2 以后会有两种可选方案,一种还是 node_modules 的方案,另一种是抛弃 node_modules,转而使用 PnP。

这两种方案前者在迁移的过程中基本不用动代码,但是已经能改善依赖安装的速度;后者需要变更的地方还挺多,在内部全部推广开来还是存在一部分阻力的,但是这种方案能够大幅度减少依赖体积以及改善安装速度,大家可以自行评估投入产出比。

代码质量保障

代码质量保障一般分为两块:

  • ESLint
  • 单测

当然还有别的质量保障方案,这里就不表了。

ESLint 其实没啥优化的方案,倒是本地提交代码的时候大部分项目应该都做了优化,只是可能很多人都忽略了。想必大家项目中应该都会存在 husky + Lint-stage,这两个工具其实能帮助我们在提交代码的时候只对需要提交的文件进行 lint。这是一种增量的思路,在很多情况下我们都需要这种思路来帮助我们做性能优化。

对于单测来说,可能很多读者压根就没写过这玩意。但是如果你做过一些 npm 包或者 Node 服务的话,会发现单测还是挺有必要的。

对于大型的项目来说,Test case 是相当多的。以我们内部的组件库为例,总共有 1000+ 的 Test case,光在本地完整执行一次 yarn test 就足足需要花费两三分钟,在云端跑的速度就更不用说了。但是实际上我们每次提交的代码影响到的 Test case 远没有那么多,每次全量跑单测花费的时间真的太多了。

说到这儿,读者们应该能记起笔者上文提到过的增量。没错,在这里我们完全可以使用增量来提高跑单测的速度。如果你使用 Jest 框架的话,可以了解下和 —onlyChanged 相关联的参数来实现增量单测。

构建

说到构建优化,想必很多读者都会说这题我会。毕竟优化 Webpack 配置已经算是面试考烂的题目了,并且市面上关于这类的文章也是层出不穷。

因此笔者这里就不再来聊我们该这样那样配置 Webpack,如果读者有需要的话可以自行网上翻阅资料。

其实除了修改 Webpack 来达成性能优化的目的,升级版本也会有很大的惊喜。

比如说从 4 升级到 5 以后,我们可以通过这些新增特性来实现提效:

  • 持久化缓存,这玩意上文已经讲过了,可以帮助我们提高二次启动及 HMR 的速度
  • 更好用的 Tree shaking 能力,能够更好地清除未使用的导出,进一步降低构建产物的体积
  • Prepack 能力,通过静态计算降低代码数量
  • 联邦模块,能够运行时加载远程模块或者依赖,减少构建所带来的时间消耗

笔者以上列举了一部分升级 Webpack 能带来的收益,大家如果对某个特性有兴趣的话可以自行搜索文章。

另外其实我们常说的构建速度优化,其中有一个点关注的人并不多,但是对构建速度也有不小的影响,那就是压缩代码。

如果你用过 Speed Measure Plugin 这个插件的话,就能发现笔者所言不虚。对于大型应用来说,就算你使用多线程进行压缩,最终可能还是会花费二三十秒的时间。

当然了,我们是能够对这个阶段做优化的,用到的工具上文也说过,也就是 ESBuild。ESBuild 主打的就是构建快,从官方的性能对比图里可以看出是降维打击其它构建器。

截屏2021-06-08下午10.34.37

恐怖的数百倍提升(当然笔者实测拿不到这样的数据),但即使它构建速度确实很快,目前还是存在了一些问题(最大的问题是 CSS 上的处理)导致上生产还是不大现实。但是实际上 ESBuild 还支持用于压缩代码,风险基本可以忽略,笔者实测业务项目中能带来 30% ~ 40% 的速度提升,还是相当可观的。

另外除了以上说的这些之外,其实在构建这个环节中我们也可以通过增量的思路来提升效率。

对于多页应用来说,大部分情况下我们每次发布所修改的代码不会影响到所有的入口。因此没有被影响到的入口实际是不需要再次被构建的,直接使用之前的缓存就行了。那么根据这个思路,我们需要每次在构建前找出上次发布到当前为止所有变动过的文件以及这些文件所影响的入口,最后动态修改 Webpack 的入口配置即可实现增量构建

说干就干,以下是增量构建的大致思路。

首先是找到距离上次发布后有变更的文件,这个很简单,一行命令就搞定了:

git diff --name-only {git tag / commit sha}

部署完别忘了打个 tag 或者记录一下 commit id,下次执行命令的时候传入。

当我们拿到变更后的文件名后,接下来需要找出这些文件所影响的入口,因此需要开始构建依赖树。虽然 Webpack 也会帮助我们构建这个,但是我们没必要用到那么重的东西,找个专注依赖树的库就行,大家可以选择 madge 或者别的类似产品。

接下来我们只需要匹配文件找到影响的几个入口就行,然后动态修改 Webpack 配置里的 entry 属性进行构建即可。

最后我们将构建的内容替换掉之前的旧入口产物就行了,没有变动的不需要管。

其实这个多页应用的增量构建做法和 monorepo 里的部署很相似。如果我们在 monorepo 里只需要对改过代码的 package 进行部署的话,那么部署代码的逻辑是很相似的,同样也是找到被影响的 package(多页应用里就是入口了),然后进行构建发布。

如果大家业务中也存在多页应用的项目,那么可以尝试下该方案,带来的收益应该会很可观。

小结

说了那么多,笔者来总结一下上文中聊过的优化手段:

  • 开发阶段尝试使用 NoBundle 替换 Webpack,效果很好,但是迁移成本需要考量
  • ESBuild 是个好东西,既能用于构建,又能用于压缩代码。前者存在风险且存在处理不佳的场景,后者风险很小,效率也能有不错的提高
  • 安装依赖提速可以从源、缓存、升级 yarn2 上着手
  • 大型项目代码质量保障阶段耗时过长,考虑通过增量方案来提速,当然如果你觉得全量跑一遍更安心也没啥毛病
  • CI 构建层面,Webpack 配置相关的说烂了,还不了解的可以自行了解,另外升级 Webpack 也会有意想不到的收益,当然迁移成本还是有的
  • 多页应用没必要每次都把所有入口构建一遍,只构建代码影响的入口即可
  • 增量思路在性能优化里相当普遍

上线后

上线后的通用性能优化也被说烂了,无非从网络协议、CSS、Webpack 配置入手,笔者还是来讲点别的。

既然要聊性能优化,那么我们肯定得知道到底哪里存在性能问题,否则就是虚空优化了。如何检测性能优化、到底有哪些性能指标也是笔者常问的面试问题(当然得面试者简历里写了做过这方面),但是大部分时候得到的答案在笔者看来是不正确的,并不能确定到底对方是不是真的做过这方面的优化。

比如说谈到性能指标,问十人九人必会说白屏时间,但是其实白屏时间在当下并不是一个合格的指标。大部分应用开屏都会存在 Loading 或者骨架屏,在这些内容过渡到页面出现用户关心的内容还需要一段时间。但是如果我们仅仅靠收集白屏时间来判断用户看到 DOM 出现是错误的做法,单靠这个指标去做开屏的优化是远远不够的,我们必须得收集到用户看到真实 DOM 的时间。

此时我们可以收集 LCP(Largest Contentful Paint)指标,这个指标会帮助我们记录页面中最大内容绘制的时间戳。

img

通过这个指标外加白屏时间,我们才能够正确的去做开屏时间的优化。另外在这里不使用 LCP 指标也是可以的,我们可以自己给关键 DOM 打点,实现个性化的收集。

除了 LCP 指标之外,还存在不少新的指标,大家有兴趣的可以了解下笔者之前写的文章,文中做几个新的指标做了阐述并说明了该如何优化这些指标。

用户体验指标

最后

以上就是本文的全部内容了。性能优化是一个很大的话题,除了那些耳熟能详的手段之外,其实还存在着不少方案能做。

大家如果有什么疑问欢迎在评论区交流。