前言
在过去几年,我曾经写过几篇和性能优化相关的文章,例如有性能优化方法相关的,有性能监控相关的。但是都只关注于局部,没有从整体上去看待、分析性能优化。所以本文打算尝试从整体上去分析前端性能优化,从性能指标,性能分析,到性能优化这一整条链路来分析前端性能优化。
在做性能优化之前,首先要了解如何评价性能好坏,不能只凭主观感受来判断。因为每个人的感受都不一样,例如首屏加载时间,有人觉得 2 秒就够快了,有人觉得 5 秒也不错。为了避免这种情况,我们要建立一个性能评价系统,在统一的标准下,分析页面性能好坏。这可以通过一系列性能指标来实现,例如 FCP、LCP、CLS 等性能指标。
对性能指标有一定了解后,就可以开始编写 SDK 来收集性能数据了,当然,也可以使用开源的监控系统。一般的监控系统都会有一个监测平台用来查看性能数据。然后我们就可以分析出来页面的性能瓶颈在哪里,再使用对应的性能优化方法来解决性能问题。
以上,是关于如何做性能优化的方法论,下面来看一下具体如何做。
性能指标
在优化性能的过程中,我们需要依赖一些具体的性能指标来对页面性能进行分析。然后通过对比优化前后的性能指标,我们可以准确判断性能优化的效果。对于性能指标,我们一般关注以下 9 个就可以了。
FP
FP(first-paint)
,从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的。
FCP
FCP(first-contentful-paint)
,从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,”内容”指的是文本、图像(包括背景图像)、<svg>
元素或非白色的 <canvas>
元素。
为了提供良好的用户体验,FCP 的分数应该控制在 1.8 秒以内。
LCP
LCP(largest-contentful-paint)
,从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP 指标会根据页面首次开始加载 (https://w3c.github.io/hr-time/#timeorigin-attribute)的时间点来报告可视区域内可见的最大图像或文本块 (https://web.dev/lcp/#what-elements-are-considered)完成渲染的相对时间。
一个良好的 LCP 分数应该控制在 2.5 秒以内。
FCP 和 LCP 的区别是:FCP 只要任意内容绘制完成就触发,LCP 是最大内容渲染完成时触发。
LCP 考察的元素类型为:
<img>
元素- 内嵌在
<svg>
元素内的<image>
元素 <video>
元素(使用封面图像)- 通过
url()
(https://developer.mozilla.org/docs/Web/CSS/url())函数(而非使用CSS 渐变 (https://developer.mozilla.org/docs/Web/CSS/CSS_Images/Using_CSS_gradients))加载的带有背景图像的元素 - 包含文本节点或其他行内级文本元素子元素的块级元素 (https://developer.mozilla.org/docs/Web/HTML/Block-level_elements)。
CLS
CLS(layout-shift),从页面加载开始和其生命周期状态 (https://developers.google.com/web/updates/2018/07/page-lifecycle-api)变为隐藏期间发生的所有意外布局偏移的累积分数。
布局偏移分数的计算方式如下:
布局偏移分数 = 影响分数 * 距离分数
❝影响分数 (https://github.com/WICG/layout-instability#Impact-Fraction)测量_不稳定元素_对两帧之间的可视区域产生的影响。
❞
❝_距离分数_指的是任何_不稳定元素_在一帧中位移的最大距离(水平或垂直)除以可视区域的最大尺寸维度(宽度或高度,以较大者为准)。
❞
「CLS 就是把所有布局偏移分数加起来的总和」。
当一个 DOM 在两个渲染帧之间产生了位移,就会触发 CLS(如图所示)。
上图中的矩形从左上角移动到了右边,这就算是一次布局偏移。同时,在 CLS 中,有一个叫「会话窗口」的术语:一个或多个快速连续发生的单次布局偏移,每次偏移相隔的时间少于 1 秒,且整个窗口的最大持续时长为 5 秒。
例如上图中的第二个会话窗口,它里面有四次布局偏移,每一次偏移之间的间隔必须少于 1 秒,并且第一个偏移和最后一个偏移之间的时间不能超过 5 秒,这样才能算是一次会话窗口。如果不符合这个条件,就算是一个新的会话窗口。可能有人会问,为什么要这样规定?其实这是 chrome 团队根据大量的实验和研究得出的分析结果 Evolving the CLS metric (https://web.dev/evolving-cls/)。
CLS 一共有三种计算方式:
- 累加
- 取所有会话窗口的平均数
- 取所有会话窗口中的最大值
第三种方式是目前最优的计算方式,每次只取所有会话窗口的最大值,用来反映页面布局偏移的最差情况。详情请看 Evolving the CLS metric (https://web.dev/evolving-cls/)。
首屏渲染时间
页面从加载开始到页面渲染完毕的时间,如果有些页面的内容特别多,我们可以只取当前屏幕展示的内容的加载时间作为首屏渲染时间。因为其他部分对于用户来说看不到,所以可以忽略那部分的加载时间。
FPS 帧率
页面的帧率就是每秒显示多少帧,一般使用 requestAnimationFrame()
来计算当前页面的 FPS。
资源加载时间
资源加载时间指的是每个静态资源文件的加载时间,通过它可以找到哪些资源加载慢,然后再分析原因。
缓存命中率
缓存命中率是用来分析你的缓存策略、内容分包机制做得好不好。一般缓存命中率越高,说明你缓存策略、内容分包机制做得越好。
接口请求耗时
通过接口请求耗时,可以分析哪些接口慢,然后再找出是服务器问题或者是接口内容过大或是网络问题。
小结
通过这 9 个指标,我们就可以对页面做到一个全面的、客观的分析,进而知道性能瓶颈出在哪。例如资源加载时间比较慢,我们可以分析出来有可能是网络问题,再进而分析是不是 DNS 有问题、或者网络带宽问题、还是 CDN 有问题等等;也可以看看是不是资源体积过大,需要进行压缩或者拆包。
总的来说,了解性能指标可以提升你对性能优化的认识,帮助你更好的实现性能优化。
性能监控
通过监控,我们可以拿到所需的性能指标数据。如果开发监控系统的压力过大,我们可以使用开源的监控系统,例如 sentry (https://github.com/getsentry/sentry) 就是一个很不错的监控平台,除了能收集性能数据,还能收集错误数据。不过如果不想使用这种大而全的监控平台,也可以选择自己开发一个监控平台,一个完整的监控平台包含了监控 SDK、数据清洗和存储、数据展示。两三个人花一两个月的时间就能开发出来一个够用的监控系统了,如果对监控 SDK 有兴趣的可以看看我这篇文章 前端监控 SDK 的一些技术要点原理分析 (https://github.com/woai3c/Front-end-articles/issues/26)。
如果不需要持续的监控页面性能,而只是想进行一次评测,那我们可以使用 chrome 浏览器自带的 lighthouse 工具来进行测试。
选择你想要收集的数据和所在平台,然后点击 Analyze page load
即可开始测试。
从上图可以看出,几个核心的性能指标都已经有了,并且还能看到整体页面和性能分数,非常的简单和直观。
小结
收集性能数据的方式基本上就是两种:
- 使用监控 SDK(数据全面,有难度)
- 使用测试工具(能收集基本的数据,使用简单)
通常情况下,测试工具就够用了,除非你的网站需要追求极致的性能和用户体验,否则不需要上监控平台。
性能优化方法
性能监控、性能指标是用来收集数据、分析性能瓶颈用的,在找到性能瓶颈后,就得使用对应的性能方法去优化它。我觉得要做性能优化,可以从两方面入手:
- 加载时(优化页面加载速度)
- 运行时(资源加载完毕后,在页面开始运行的时候进行优化)
所有的性能优化手段都可以归纳为以上两类。其中对性能影响最大、收益最大的是在页面加载时进行优化。
从上图可以看到,一共有 23 条性能优化方法,其中加载时优化有 10 条,运行时优化有 13 条。由于篇幅有限,这 23 条性能优化方法不能逐一展开细讲,就讲讲其中的一条吧 「使用 CDN」。
静态资源使用 CDN
内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
CDN 原理
当用户访问一个网站时,如果没有 CDN,过程是这样的:
- 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
- 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到网站服务器的 IP 地址。
- 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。
如果用户访问的网站部署了 CDN,过程是这样的:
- 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
- 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。
- 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。
- 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
- SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
- 浏览器再根据 SLB 发回的地址重定向到缓存服务器。
- 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。
小结
由于篇幅有限,文中提及的一共 23 条性能优化方法可以看我的这篇文章前端性能优化 24 条建议(2020) (https://github.com/woai3c/Front-end-articles/blob/master/performance.md)。
性能优化实战
在所有性能优化方法中,收益最大的有三个:压缩、懒加载和分包、缓存策略。绝大多数的应用做了这三个优化后,页面性能都能得到极大的提升。
压缩
压缩包含代码压缩和文件压缩,常用的代码压缩工具有 terser、uglify,文件压缩一般使用 gzip。1m 的文件使用 gzip 压缩后体积一般是 300-400kb。
压缩通常情况都是在前端打包的时候做,然后将打包好的文件上传到服务器。用户访问网站时,nginx 先查看用户访问的资源是否有 .gz 后缀的文件(gzip 压缩后的文件),如果有就返回,没有就自己压缩后再返回。这一块需要在 nginx 上进行配置才支持,默认不开启。尽量不要让 nginx 压缩,因为访问量高的时候,对服务器压力很大。
压缩是对整体项目的体积大小进行优化。
懒加载和分包
路由懒加载基本上每个前端都会,但是某些系统仍然需要手动优化。比如一些构建工具会默认把所有的第三方包都打在 verdor.js
文件里。但实际上并不是所有的页面都需要把所有的第三方包都加载进来,所以需要做好具体的分包策略来进行优化。分包主要是为了对打包好的资源进行拆分,让一些不是所有页面都要使用的资源做到懒加载。
懒加载和分包不改变项目的体积大小,而是将一些不是那么重要的资源放到后面再加载。比如有 10 个页面的资源,每个 1m,假设加载时间为 1 秒,都做成懒加载后,每次访问页面只需要一秒,而不是都打包在一起,首次访问要 10 秒。
缓存策略
浏览器缓存分为「强制缓存」和「协商缓存」,强制缓存就是缓存生效期间,浏览器会直接使用缓存的资源,而不会发起请求。协商缓存就是浏览器发起请求后,发现文件未变化,然后再使用缓存。
缓存策略就是围绕着强制缓存、协商缓存做的优化方案。在做优化之前,还要给大家再介绍一下缓存命中率。
缓存命中率=缓存命中的资源/所有请求的资源
。缓存命中率越高,说明你的缓存策略做得越好。
缓存策略一般通过 nginx 来实现,当然也可以自己手写代码实现。现在常用的缓存策略是:
- 打包文件时所有文件名称按照文件内容进行 hash 命名,当内容改变时文件名也会改变
- 在 nginx 配置 html 文件为
cache-control: no-cache
,每次浏览器请求 html 时都要向服务器发起请求,查询 html 文件是否有变化,即协商缓存 - 其他所有静态资源文件设为长缓存(例如缓存时间为一年),即强制缓存
这样优化后,除了首次打开页面需要加载静态资源,后面再打开页面就不需要了,所以能在瞬间打开页面。当项目代码更新后,相关的 js、css 资源文件会发生变化,其他不变的部分,代码不会变化。当资源文件发生变化后,html 也会发生变化(引用的资源文件名称变了)。然后浏览器会重新加载 html 中发生变更的资源,没有变化的资源会使用缓存。目前只有 webpack 支持根据文件内容生成 hash,所以说 vite 是代替不了 webpack 的,它们会同时存在。
想了解如何利用 webpack 实现文件精确缓存的,可以看我这篇文章:webpack + express 实现文件精确缓存 (https://github.com/woai3c/node-blog/blob/master/doc/node-blog7.md)
现在我们再看一下其他比较知名的网站是怎么做缓存的。
一些比较好例子:
- www.zhihu.com/ (https://www.zhihu.com/)
- bbs.hupu.com/all-nba (https://bbs.hupu.com/all-nba)
- www.google.com/ (https://www.google.com/)
不好的例子:
- 省略…
为了避免不必要的问题,不好的示例还是不帖了,但是大家可以找一些小网站或者小公司的官网看看,基本上都没怎么做性能优化。
另外,有些细心的同学可能会发现即使没有设置 cache-control
,浏览器也会进行缓存。这是因为没有设置 cache-control
响应头时,浏览器将自行选择缓存策略。
「浏览器默认行为」:即使响应头部中没有设置 Cache-Control
和 Expires
,浏览器仍然会缓存某些资源,这是浏览器的默认行为,是为了提升性能进行的优化,每个浏览器的行为可能不一致,有些浏览器甚至没有这样的优化。
性能优化的成本和收益
在绝大多数的单页面应用(SPA)中,页面采用懒加载的策略,并且通过打包工具实现代码压缩,基本上不会有性能问题。但是随着项目规模的扩大,可能会出现性能问题,这时就需要做性能优化了。
但是,性能优化并非无止境的进行。我们需要充分考虑优化的成本和收益,而不是盲目地采用所有可能的优化方法,因为这将带来过高的开发和维护成本。只有当项目的性能表现无法满足预设的性能指标时,我们才需要进行进一步的优化。这样,我们可以在保证项目性能的同时,也能控制好优化的成本。
举例,性能优化实战中说到 vite 不能实现按内容进行 hash。也可以打包时用 webpack 来替代 vite。但成本比较高,没什么必要,除非对性能有极端的要求。
疑难解答
不好的代码是否会影响性能优化
如果做好了项目的整体架构设计(结合性能优化方法论、前端工程化、合理的研发流程、代码规范和 CodeReview),不好的代码对性能影响很小,除非写一些死循环代码。当然,前端工程化这些概念非常宽泛也不好理解,有兴趣可以看我写的这一本电子书:带你入门前端工程 (https://woai3c.github.io/introduction-to-front-end-engineering/)
举例,一个页面几千行代码会不会影响页面性能。如果这个页面确实需要这些代码,那说明代码总量不会变,再怎么拆分还是那么多代码。所有代码写在一个页面只会影响代码可读性,而不会影响性能。
总结
关于性能优化的内容已经说完了,但是我还想说点别的,主要想说一下我们应该学习哪些技术才能让它更加保值。
在我看来,越偏向于应用层面的技术越容易过时,越偏向于底层的技术越不容易过时。 性能优化方法论是偏向于底层的,它不容易过时。为什么呢?
因为对于性能优化的理解和掌握,更接近于计算机科学的底层原理。这些原理包括但不限于算法复杂度、内存管理、数据结构的选择和使用、计算机网络知识等。这些基础理论并不会因为应用环境或者具体的技术实现的变化而变得过时。
虽然前端技术的发展非常快,也涌现出很多的框架(例如 HTML4 到 HTML5 的升级,或者从jQuery 到前端三大框架的转变),但是性能优化的基础理论是相对固定的。例如,无论在什么情况下,减少 HTTP 请求、合理利用浏览器缓存、代码压缩和合并、选择正确的数据结构和算法等都是通用的优化策略。
因此,从这个角度来看,理解和掌握性能优化的理论,是一种更为根本的、不易过时的技能。而且这种技能不仅适用于前端开发,也同样适用于其他计算机科学的领域。
我们学习技术也一样,应用层面的技术不要出一个新东西就去学,通常情况下只学对工作有用的就可以了,避免浪费时间。剩下的时间尽量去学习偏底层的技术,从前端的角度来看,可以学习脚手架、性能优化、微前端、监控、浏览器渲染原理等知识。当然,我更建议大家成为一个全栈,不要把自己的定位局限于前端。
参考资料
- Performance API (https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_API)
- PerformanceResourceTiming (https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming)
- Using_the_Resource_Timing_API (https://developer.mozilla.org/zh-CN/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API)
- PerformanceTiming (https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming)
- Metrics (https://web.dev/metrics/)
- evolving-cls (https://web.dev/evolving-cls/)
- custom-metrics (https://web.dev/custom-metrics/)
- web-vitals (https://github.com/GoogleChrome/web-vitals)
- PerformanceObserver (https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver)
- Element_timing_API (https://developer.mozilla.org/en-US/docs/Web/API/Element_timing_API)
- PerformanceEventTiming (https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming)
- Timing-Allow-Origin (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Timing-Allow-Origin)
- bfcache (https://web.dev/bfcache/)
- MutationObserver (https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver)
- XMLHttpRequest (https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)
- 如何监控网页的卡顿 (https://zhuanlan.zhihu.com/p/39292837)
- sendBeacon (https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon)
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/88439.html