页面的性能优化对于前端来说永远是离不开的课题,前端性能优化一直也不是作为一个单独的问题存在,它往往需要开发者结合计算机网络、浏览器相关技术、前端框架、构建工具以及开发者自己的代码等多层面去思考优化的方案,所以前端性能不应该是前端领域的一个孤岛⛱️,而需要作为串联起前端技术的零件。
一说到前端性能优化,可能大家一开始的想法就是压缩页面产物大小、图片换成雪碧图、资源懒加载等一系列方式,但是要知道性能优化并不是直接套用方法论的,性能优化的前提清楚需要页面需要优化的指标,并根据有方向有目的进行优化。
古早时期,大家常常会把性能优化与页面白屏时间划上等号,但是白屏时间短并不代表页面性能就好,例如页面进去是渲染的是loading动画又或者是一个可有可无的模块,虽然白屏时间是短了,可以渲染出来的内容对用户来说是毫无意义的,美其名曰提升了首屏性能,实际上不过是掩耳盗铃罢了。
对大多数页面来说,性能优化的的宗旨还是确保用户可以尽快的看到有用的页面信息,那页面加载到什么程度才能够算是渲染出有用的页面信息呢?
FMP(first meaningful print)即首次有意义的渲染,这个指标感觉真的和我们所说的有用的页面信息是完美契合。
我们可以看到在FMP的阶段,页面中大部分元素其实都被渲染出来,我们来看看这个值到底是怎么被计算的。
在页面渲染和解析的过程中,布局对象会被逐步添加至布局树中,从上图可以看出布局对象的数量和页面完成度是高度相关的,所以业界比较认可的计算方式是页面在加载和渲染过程中最大布局变动之后的绘制时间作为当前页面的FMP,上图对应的FMP的值为1.907s。
通常的检测手段是使用MutationObserver监听页面整体的DOM变化,然后通过计算变化比例,找到DOM变化幅度占比最高的时间点。
FMP需要浏览器支持MutationObserver API,并且FMP的计算过程是十分复杂的。在我们团队开发的时候会比较hack,页面直接上报的是首屏接口被处理后React setState后的时间或者是首屏大图渲染的时间,而其实这个时间并非是页面精准的FMP值。
在lighthouse 6.0的性能规范中,废弃了FMP的这个指标。官方给到的解释主要有两点:
相比于 FMP 计量的复杂和不确定性,W3C性能小组和Google研究发现,衡量页面主要内容加载更准确方法是查看最大元素的呈现时间。也就是lighthouse的指标 LCP 。
LCP(Largest Contentful Paint) 指的是视口中可见最大图片或文本块的渲染出来的时间。所谓的最大图片或文本块包含以下内容:
但是一个真正用户体验好的页面并不仅仅只是页面LCP数值好就代表真的体验好,根据最新Google最新的页面性能的核心指标标准显示,除了LCP外,还包括互动响应指标 INP(Interaction to Next Paint) 以及布局偏移指标 CLS(Cumulative Layout Shift),这三个指标共同组成了页面核心性能指标。
INP(Interaction to Next Paint)是使用 Event Timing API 中的数据来评估网页响应能力。INP 会在网页生命周期内观察用户与网页进行的所有点击、点按和键盘互动的延迟时间,并报告最长持续时间。
INP大家可能会感受到陌生,可能更熟悉的指标是FID(First Input Paint),FID用户首次与页面交互(例如点击链接、按钮等)到浏览器实际能够开始处理事件处理器之间的时间。2年前这个指标还是lighthouse检测页面交互性的核心指标,如今将被INP取代了。
INP 会统计网页生命周期内观察用户与网页进行的所有点击、点按和键盘互动的延迟时间。而FID仅测量了页面上首次互动的输入延迟,所以我认为可以等价理解为FID像是成为了INP的子集。
对于纯渲染的页面来说,其实INP记录的交互时间的最大值可能就是FID,因为此时大量的JS执行,以及DOM生成,以及样式渲染,此时主线程肯定会被长时间占用的。所以对于纯渲染的页面可能考虑首屏的INP指标。但是对于一些有复杂交互的页面,例如文本编辑器或联动效果的的表单页面,可能首屏内容渲染是很快的,但是可能后续的互动时间会比较耗时,所以此时FID是不能衡量互动性能的,此时INP更能体现页面的交互性能。
首先INP指标的总耗时主要包括三个方面:
这里可以借用web.dev网站提供的一个案例了解下如何在日常开发中让主线程,拆分掉冗长的事件回调。
同步布局: 强制同步布局是指在执行 JavaScript 或者 CSS 动画过程中,代码强制浏览器进行布局计算(Reflow),然后再读取某些样式信息。 例如,如果一个 JavaScript 函数对 DOM 进行修改后立即读取某些样式属性(如元素的偏移量或尺寸),浏览器必须先完成布局计算,以确保返回的信息是最新的。这种强制的布局过程可能会导致显著的性能瓶颈,因为它阻塞了主线程,直到布局计算完成。
布局抖动: 布局抖动通常是由于代码在一次事件循环中多次读写 DOM 属性而导致的连续布局计算。每次读取或写入都可能导致布局的重新计算,如果这些操作在循环或频繁的函数调用中进行,就会导致大量的计算开销,从而降低页面性能。
CLS(Cumulative Layout Shift)是用于衡量视觉稳定性的重要指标,它有助于量化用户遇到意外布局偏移的频率。
首先我们看看CLS的计算公式:
CLS值 = 偏移比例(偏移的距离占视窗的距离的比例)* 元素比例(元素高度占视窗的高度的比例)
接着我们根据一个具体案例了解下CLS的计算过程:
(1) 起初页面加载出了粉色的一个div块。
(2)之后黄色模块再加载出来。
我们假定粉色模块的高度占视窗的50%,所以元素比例为0.5,然后假定黄色模块占视窗的20%,其造成的偏移距离也是20%,所以偏移比例为0.2。 最终CLS的值等于0.5 * 0.2 = 0.1。CLS低于0.1可以代表视觉稳定性比较好。
不知道会不会有些人和我一样有这样的困惑。我的页面可能有以下场景,例如点击展开,又或者是搜索框展示提醒之类的,用户的一些交互输入导致页面偏移,这些情况导致的偏移都会被统计到CLS中去么?
于是我迅速的翻阅了下 web.dev 的相关文档,文档是这么阐述的,对于在用户输入后 500 毫秒内发生的布局偏移,系统会设置 hadRecentInput 标志,因此这段时间发生的偏移会从计算中排除。
因为这些用户主动去进行的输入,说明用户是对偏移有预期的,但是例如页面加载的时候,资源加载缓慢导致的偏移,这种才会被理解为意料之外的偏移,最终被统计到CLS的指标中去。
(1) 预设高宽的图片或者是前端模块组件。这些图片或者组件可能依赖的网络请求所以存在延时,在数据到达时导致页面偏移。
(2) CSS动画尽量使用translate和transform 仅影响合成阶段的属性。如果直接操作元素的 top 或 left等属性,会触发页面的重新布局、绘制和合成。所以需要修改元素的位置可以使用translate,而修改大小可以使用transform,这两个属性只会影响合成阶段,不会影响布局阶段。
(3) 预加载的字体资源。加载字体资源由于也需要网络请求,所以在字体资源加载生效之后,导致页面的内容偏移。而且字体比较特殊,需要在页面上使用时才会加载,所以为了尽快使用上字体文件,可以使用preload预加载资源。
其实性能优化是一个十分需要耐心的工作,我们需要根据各种指标了解到我们页面的不足之处,然后再围绕具体指标的优化策略去对症下药,而不是看到网上有什么优化策略,就依葫芦画瓢。因为每个项目的场景,优化诉求都不同,例如toC的H5页面可能追求更好的LCP指标和CLS指标,而toB的CMS应用追求更好的INP指标。
个人觉得性能指标应该是作为我们作为性能优化的导向,让我们有性能优化的方向,而不是衡量页面性能的绝对标准。有时候不同机型、不同网络情况优化的效果可能也不同,所以我们也不用过度的追求lighthouse的最终得分。
所以前端到底要怎么去优化性能? 我认为结合自己项目的实际情况,按照相应的指标对症下药去优化就好了。