Web Vitals - 以用户为中心的性能指标
前言
本文主要内容是讲解本人对
web-vitals
指标的理解,偏理论性一点,趋向会比较乏味。
web-vitals
是 Google 团队在 2020 年开创的一项新计划,这个计划的核心是站在站在用户角度的来衡量一个网站的真实体验。其实在这之前也有一些 W3C 定义的像 DOMContentLoaded 之类的加载指标,只是它们大多是站在开发者的角度。而本文要讲的核心以及重要的 WEB 指标都是以用户为中心的。
核心 WEB 指标
上图我们从左往右看,我们会发现每一个指标它的单位其实都是不一样的,有秒的,毫秒的,纯数值的。
其实我们能看到每一个指标最底部是这些指标的参考值。反观每一个指标顶部都有一个灰色的小字,它则是对应以下类别。
- 加载
- 互动性
- 视觉稳定性。
顺带一提,谷歌建立这个计划背后的目的是用于谷歌浏搜索引擎的 SEO,在 2021 年这些指标已经加入到权重计算中了。
LCP
Largest Contentful Paint 最大内容绘制
LCP 要满足以下几个重点,最重要的是最大
- 可视区域内
- 页面首次加载
- 图像或文本
- 最大的
哪些元素会被加入到计算范围内?
- 文本
- 包含文本节点或其他行内级文本元素子元素的块级元素
- 图片
- 通过 url() 函数加载的带有背景图像的元素
- 内嵌在 SVG 元素内的 image 元素
- Video 元素的封面图片
- 常规的 IMG 标签元素
顺带一提,谷歌打算未来将会加入 SVG 和 Video 元素。
如何确定一个元素的大小?
- 实际显示大小为准
- 文本矩形框大小
- 边框填充不考虑
TIPS: 实际显示大小为准,比如一个图片原始尺寸很大要下载很久,结果你用 css 缩放到很小,那这时候它并不会是这个页面上最大的元素。如果是文本框的情况下,就只考虑当前节点的矩形框框大小。所有的元素你用 css 填充边框之类的都不在考虑范围内。
什么时候收集这个最大的元素合适?
我怎么在代码内收集?
// 谷歌官方库
import { getLCP } from 'wev-vitals';
getLCP(console.log);
// 原生 JS 获取
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log(`LCP candidate: ${entry.startTime} ${entry}`);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
FID
First Input Delay 首次输入延迟
这里的输入是指首次交互,那么怎么样才会采纳为首次?
- 不连续操作的输入事件
- 常规点击、轻触和按键
- 不采纳滚动和缩放这种连续操作
FID 耗时是怎么产生的?
FID 好像很难理解又好像很好理解。其实 FID 确实很简单,它就是下图画的时间线那样的。因为主线程处于忙碌状态,所有有些场景下可能并不能立刻响应,虽然这种场景很少。
如果交互没有事件侦听器怎么办?
其实这跟你的事件监听不监听毫无关系,它的运作流程是用户发生点击之后, 它去查浏览器的主线程是否空闲。如果不空闲,就等到空闲则为 FID 的时间。
那如果没有发生过被采纳的交互,是不是就不存在 FID?
是的,这个值比较特殊,是用户驱动的,并不是浏览器自驱动的,也符合我们的大标题。 比如一个用户点错进来这个页面又立刻左滑返回。这个时候是不存在 FID 时间的。
我怎么在代码内收集?
// 谷歌官方库
import { getFID } from 'wev-vitals';
getFID(console.log);
// 原生 JS 获取
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const delay = enrty.processingStart - entry.startTime;
console.log(`FIS candidate: ${delay} ${entry}`);
}
}).observe({ type: 'first-input', buffered: true });
CLS
Cumulative Layout Shift 布局累计偏移,量化。
为什么需要 CLS
英文翻译过来是布局累计偏移,量化是我自己补上去的。这句话看起来很难理解。 我们看下面的效果图会更好理解。
我们可以从动图里面看到,本来用户想点 NO 的,结果在点击的瞬间页面的布局发生了变化,导致点了 YES。 如果 CLS 控制得差,可想而知这用户体验有多糟糕,至于页面的布局为什么会变化开发者都清楚。
CLS 是怎么计算的?
偏移距离 * (偏移距离 + 元素高度) = CLS
上图的 CLS 值为 0.2178,0.33 这个数值是页面高度的 33%,所以是 0.33,该例子刚好偏移距离与元素高度相等。
怎么在代码内收集?
// 谷歌官方库
import {getCLS} from 'web-vitals';
getCLS(console.log);
// 原生 JS 获取
let clsValue = 0;
let clsEntries = [];
let sessionValue = 0;
let sessionEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// 只将不带有最近用户输入标志的布局偏移计算在内。
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// 如果条目与上一条目的相隔时间小于 1 秒且
// 与会话中第一个条目的相隔时间小于 5 秒,那么将条目
// 包含在当前会话中。否则,开始一个新会话。
if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// 如果当前会话值大于当前 CLS 值,
// 那么更新 CLS 及其相关条目。
if (sessionValue > clsValue) {
clsValue = sessionValue;
clsEntries = sessionEntries;
// 将更新值(及其条目)记录在控制台中。
console.log('CLS:', clsValue, clsEntries)
}
}
}
}).observe({type: 'layout-shift', buffered: true});
重要的 WEB 指标
FCP
First Contentful Paint 页面首次内容绘制
只有文本、图像(包括背景图像)、<svg>
元素或非白色的 <canvas>
元素能被纳为页面首次内容。
FCP 就是如下图所见的。第一个页面中间的 SVG,也是它的 Loading,这个元素开始渲染的时间就是这个页面的 FCP 值。它的特点跟 LCP 一样,只参照文本或图像。
顺带一提,还有一个比 FCP 更前的时间,叫 FP,其实就是 html 首个字节开始渲染的时间。
TTI
Time to Interactive 可交互时间
什么是 TTI,TTI 就是可交互时间,这样说可能有点抽象。准确的来说是在 TTI 标记之后的时间都是可交互时间。 那么 TTI 是怎么计算的呢?我们看下图。这个图,浅绿色的是任务块,黄色的是长任务。顶部是请求任务的进度线。
计算方式
- 获得 FCP
- 向右查找不少于 5 秒的安静窗口
- 向左查找最后一个长任务的结束时间
- 如果没有则 TTI = FCP
在获得 FCP 的时间之后,从 FCP 往后也是时间轴的向右找。找到一个不少于 5 秒的安静窗口,也就是图中的灰色块。在这个窗口的起始位置往左找,图中那个 3 的那条箭头的线就很明确,找到第一个长任务的结束时间节点。这个时间节点就是 TTI 的值。
长任务在主线程执行大于 50 ms 的任务
安静窗口为没有长任务且不超过两个正在处理的网络 GET 请求
为什么需要 TTI?
像一些不依赖框架运行时,而是利用 SSR 来渲染的站点,它们就像对容易给用户造成现在已经可以开始交互的错觉。这种情况可以通过 TTI 的数值实际的量化真正可以开始交互的时间节点!
TBT
Total Blocking Time 总阻塞时间
前面我们讲了 TTI,我们理解到 TTI 是一个我知道什么时间之后用户发生的交互是可以相对比较快的响应的。那 TBT 就是去量化 FCP 至 TTI 之间,实际可能给用户带来的总交互阻塞时长。 看上面的图,还是一样时间轴从左往右走。这条线代表主线程,每一个黄色区块都是主线程的任务,棕色代表阻塞时间。比如第一个区块,它的阻塞时间是 200ms,任务执行总时间是 250ms。它超过了 50ms,所以是一个长任务。为什么阻塞时间只算 200ms,因为小于 50ms 的响应时间,被我们主观认为用户是感知不到的。
如何计算?
还是上图为例,一共五个块,棕色代表阻塞的时间 / 黄色代表主线程的任务
- 200ms / 250ms
- 90ms / 40ms
- 0ms / 35ms
- 0ms / 30ms
- 105ms / 155ms
以此类推的话,该图片例子整个总阻塞的时间加起来就是 345ms,这也就是 TBT 的时间。OK,这就是 TBT,也就是我们可以重点考虑优化的时间空间!
自定义指标
一些可能在常规情况中用到的自定义指标的收集代码示例
监听元素渲染时间
我们在一些相较特殊的业务中,LCP 并不是我们认为的加载完成。我们有比 LCP 计算规则更重要的元素。 我们就可以自定义的去监听元素是何时渲染的。
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
...
<script>
const ob = new PerformanceObserver((list) => {
const perfEntries = list.getEntries();
// 做你想处理的事...
});
ob.observe({ type: 'element', buffered: true });
</script>
监听资源加载时间
有一些时候我们可能会比较关心某些资源的加载情况,比如说 A/B 测的情况下,我们想知道 WEBP 的用户真的比 PNG 的用户快吗?这个时候就可以去收集资源的加载情况了。
const resourceList = performance.getEntriesByType("resource");
for (let i = 0; i < resourceList.length; i++) {
const { initiatorType, responseEnd, startTime } = resourceList[i];
if (initiatorType === "img") {
const timeline = responseEnd - startTime;
console.log(`图片的加载时间线: ${timeline}`);
}
}
监听长任务时间
监听长任务这个大概率是用于代码的优化或者磨平 TBT 的时间。
const ob = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.toJSON());
}
});
ob.observe({type: 'longtask', buffered: true});
结束
总结
我自己写到这的时候发现好像没什么好总结的,正如我开篇所说的这是一个很理论的东西。 在我的角度来讲,如果能让你知道有一个以用户为中心的指标,它是谷歌修订的。 大概有个印象,这对我来说我的任务已经完成了。
参考链接
- https://cloud.tencent.com/developer/article/1900501
- https://web.dev/user-centric-performance-metrics/#in-the-lab
- https://web.dev/custom-metrics
- https://web.dev/lcp
- https://web.dev/fid
- https://web.dev/cls
- https://web.dev/fcp
- https://web.dev/tti
- https://web.dev/tbt