kd-cloud-web / Blog

一群人, 关于前端, 做一些有趣的事儿
13 stars 1 forks source link

常见的 Web 性能指标介绍和测量 #79

Open Luin-Li opened 9 months ago

Luin-Li commented 9 months ago

常见的 Web 性能指标介绍和测量

当我们通过 Chrome Dev Tools Performance 工具对页面进行性能分析时,会出现一些关键的 Web 性能指标的标记点。我觉得使用Performance 工具进行性能分析和优化的前提是,对这些关键指标有一定了解。本文不会侧重介绍 Performance 工具的使用,主要介绍一些常见的 Web 性能指标。

performance

如上图为使用 Performance 工具监控百度搜索首页的加载。BTW: Main 面板的火焰图有部分 Task 右上角有标红,这说明该 Task 耗时较长, 代码逻辑处理或分配可能存在问题。

网站的三大性能指标

cwv

Google 于 2020 年 5 月提出一组 Web Vitals 指标,该组指标主要用来衡量网站是否具有良好的用户体验。如上图为 2023 年以来,Google 根据该组指标统计的各大前端框架开发的网站的性能对比。

web-vital-metrics

百分位的含义:

为确保 大多数用户 都能达到此目标,会根据网页或网站所有浏览量的第 75% 的值作为衡量标准,即如果某个网站至少有 75% 的浏览量达到“不错”的阈值,则根据该指标,该网站会被归类为“不错”的效果。例如,这个网站有 75% 的浏览量 LCP 值为2秒,则该网站LCP性能为良好。

为什么不是100%?如果对某个网站的几次访问恰好位于不稳定的网络连接上,从而导致 LCP 样本过大,我们并不希望根据这些离群样本决定网站分类。(个人理解,类似正态分布取值)

接下来这三个性能指标的测量都会用到 PerformanceObserver 这个 API,这里也可以先对这个 JavaScript 对象进行简单了解。除此之外,在对页面关键性能指标进行记录时,也可以使用web-vitals这个谷歌提供的开源库。

LCP:最大内容绘制

lcp

Largest Contentful Paint (LCP):视口内可见的最大图片或文本块的呈现时间。

为什么是2.5秒?

根据 “1秒阈值” 的定义,人类对某种刺激作出的响应大约在 1 秒(即大约 0.3-3 秒)内。进一步解释,用户在失去焦点之前将等待的时长大约为 0.3-3 秒,也就是说良好的 LCP 阈值应该在0.3-3 秒中选一个。LCP 通常发生在 FCP 之后,而现有的良好的 First Contentful Paint (FCP) 阈值为1.8秒,所以良好的 LCP 阈值又被限制在1.8-3 秒之间。关于什么是FCP下文也会展开介绍。

最后基于整个网络中表现最好的网站的 LCP 性能和实现的可能性,选定了2.5 秒作为良好的LCP阈值。我们发现,1.5 秒和 2 秒的阈值并非始终能够实现,而 2.5 秒的阈值却始终能够实现。

哪些元素会影响 LCP 的值? 文本相关的元素或节点、图片相关的元素,包括<img>元素、通过url()加载的背景图片等。

关于元素的大小

Largest Contentful Paint 的选择

我们知道浏览器对资源的加载是分优先级的,最开始浏览器可能渲染文本,接着可能是图片等元素。这意味着 网页上最大的元素可能会发生变化。 针对这种情况,浏览器会根据绘制的进展,持续分派新的 PerformanceEntryPerformanceEntry是一个浏览器提供的 performance API。

元素只有在渲染并对用户可见后,才能被视为最大的内容元素。一旦用户与页面交互(通过点按、滚动或按键),浏览器就会停止报告。

使用 JavaScript 测量 LCP

将下面代码在console面板运行,我们就可以看到这个页面所有的LCP条目。通常最后一个条目的 startTime 值是 LCP 值。

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

如下图为百度搜索首页的 LCP 测量结果为0.47s。

baidu-lcp

baidu-lcp-result

FID:首次输入延迟

fid

First Input Delay(FID): 用户首次与网页互动 (如点击) 到浏览器实际开始处理脚本,以响应该互动的时间。

关于为什么是0.1秒?这里不展开叙述,有兴趣可以查看原文。 总之, 0.1 秒的阈值能让用户感觉到系统的响应是瞬时的。

为什么会有 FID? 之所以发生输入延迟,是因为浏览器可能正在解析和执行其他 JavaScript 文件。

fid-tti

FID 通常发生在 FCP 和 TTI 之间。*Time to Interactive (TTI):从网页开始加载到主要子资源已加载且能够快速可靠地响应用户输入的时间。* 我们都知道,JavaScript 引擎是单线程的。如上图所示,在 JavaScript 文件下载完成后,会在主线程开始处理,这会导致主线程处理忙碌状态。假设用户在耗时最长的任务途中忽然开始于网页交互,那么他必须等到该任务处理完成后,浏览器才能响应该次交互。这个时间差是用户必须等待的时间,即FID。

这里需要注意两点:

  1. 除了事件监听函数,有些HTML元素也需要等到主线程任务完成后才能响应用户互动,包括<input><textarea><select><a>
  2. FID 仅仅测量的是 延迟时间,不包括这个事件处理本身需要的时间和处理后浏览器更新界面的时间。

使用 JavaScript 测量 FID

FID = processingStart - entry.startTime。将下面代码在 console 面板运行。

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

如下图,在百度搜索页加载出来后,点击输入框获取焦点,测出其 FID 为1.49ms。

baidu-fid

CLS:累积布局偏移

在图片等资源异步加载的过程中或者在 DOM 元素被动态添加到页面过程中,通常会发生网页的布局变动。如果此时用户正要点击页面的某个元素,那可能就会发生误点,带来不好的用户体验。

cls

Cumulative Layout Shift(CLS):用于衡量页面的可见内容偏移的程度。

每当可见元素的位置从一个渲染帧更改为下一个渲染帧时,都会发生布局偏移。CLS 衡量的是页面的整个生命周期内发生的每次意外布局偏移的最大突发性布局偏移分数。

什么情况下视为发生了布局偏移?

当现有元素更改其起始位置时,才会发生布局偏移, 添加新元素或者现有元素更改尺寸不计入布局偏移中。布局偏移的计算与Layout Instability API 有关,有兴趣的可以查看其文档进行进一步了解。

布局偏移分数 = 影响分数 * 距离分数影响比例 为变动大小占总视口的百分比,即两个帧变动块的并集。**距离分数* 为不稳定元素相对于视口的 移动距离** 占总视口的百分比。

如下图所示,第二帧新出现的黄色按钮导致绿色背景的文字块向下移动,使其部分移出视口,但是计算影响分数时,不考虑不可见区域,所以两个帧绿色块可见区域的并集即为第一帧绿色块面积,即占总视口的14%,即影响分数为0.5。移动距离为紫色箭头大小,约占总视口的14%,即距离分数为0.14。所以布局偏移分数为 0.5 * 0.14 = 0.07

cls-eg

当然,布局偏移肯定是无法避免的,例如用户的交互触发了一些布局和网页内容的变动,但是我们可以通过预先占好空间、显示进度加载组件、添加过渡动画等,帮助用户更好了解接下来发生了什么。如下图,对于在用户输入后 500 毫秒内发生的布局偏移,系统会设置 hadRecentInput 标志,因此可以将其从计算中排除。

recent-input

使用 JavaScript 测量 CLS

在实际测量中,在大多数情况下,卸载网页时的当前 CLS 值是该网页的最终 CLS 值。

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Layout shift:', entry);
  }
}).observe({type: 'layout-shift', buffered: true});

如下图为,百度搜索结果页加载过程中,发生的布局偏移。

baidu-cls

Performance 面板

除此之外,通过 Performance 面板 Record 之后 Experience 项也可以查看页面操作过程中的所有 Layout Shift

如下图为,在百度搜索结果页通过 Tab 切换搜索内容类型(网页->资讯->贴吧)这几个操作过程中发生的所有CLS。

cls-baidu-perform

其他

DCL: DOMContentLoaded

从事件循环角度看这个时间点,如下图,它大概是在 GUI 线程的DOM Tree这个节点触发的,此时HTML 文档被完全加载和解析完成,DOM 树已经构建完毕,JavaScript 可以访问所有 DOM 节点,但是像是 img 和样式表等外部资源可能并没有下载完毕。

gui

每个浏览器 Tab 页默认有一个渲染进程,当 JavaScript 引擎线程执行时,上面提到的 GUI 线程就会被挂起。也就是说,JavaScript 的加载和执行的时间是可能会影响到 DOMContentLoaded 事件触发的时间点的。通过下面示例帮助理解,这个 html 文件需要如下加载三个脚本文件。

<script src="./main.js"></script>
<!-- 下面main.js这个外部文件需要较长的加载时间 -->
<script src="http://localhost:3000/main.js"></script>
<script src="http://localhost:3000/test.js"></script>

默认情况DOMContentLoaded 会等所有 JavaScript 下载和执行完成后才触发。默认情况下这三个脚本文件的优先级是一样的,都是High

default

异步加载情况:首先异步加载的资源浏览器的优先级会调整为 Low

BTW: asyncdefer都存在的时候,以async为准。两个同时存在的情况,主要用于解决兼容问题。

// 情况一
<script src="./main.js"></script>
<script src="http://localhost:3000/main.js" defer></script>
<script src="http://localhost:3000/test.js"></script>

defer

// 情况二
<script src="./main.js"></script>
<script src="http://localhost:3000/main.js" async></script>

async

总结:必要时,给一些加载较耗时的脚本添加 async 标签是可以降低 DCP 时间的。

FCP:首次内容渲染

First Contentful Paint(FCP):用于测量从网页开始加载到网页任何一部分内容 (可能是图片、文本或<canvas> 呈现在屏幕上的时间。

FCP 是一项以用户为中心的重要指标,它标志着网页加载时间轴中用户能看到屏幕上任何内容的第一个点。我们常说的首屏时间 (First Paint Time FPT)首屏结束时间 = FCP 事件触发时间。我们通过 Performance 工具监控一次页面加载过程时,也可以看到 FPFCP 两条时间线是重合的。

使用 JavaScript 测量 FCP

将下面代码在 console 面板运行, 测出其 FCP 为 925ms。

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
    console.log('FCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'paint', buffered: true});

如下图测出百度搜索首页 FCP 为508ms。

baidu-fcp

其他:FP 和 Load

First Paint(FP):首次渲染时间。我们常说的**白屏时间**(Blank Screen Time BST)白屏结束时间 = FP 事件触发时间

Load:Load 事件触发时间。此时,在 HTML 中的所有图片、脚本和样式表等资源都加载完毕。

--

参考资料

Core Web Vitals Technology Report

Web Metrics

浏览器加载

补充:PerformanceObserver API

PerformanceObserver 是一个用于监测网页性能的 JavaScript API。它允许开发者监听和捕获与页面性能相关的事件,如资源加载、绘制、浏览器内部事件等,以便更好地了解和优化网页性能。该 API 大约在 2017 年被各大浏览器引入,如下图为这个 API 的浏览器支持情况:

performance-api-caniuse

如何使用

如上面测量LCPFIDCLSFCP的示例代码,大致分为三步:

  1. 创建 PerformanceObserver 实例

const observer = new PerformanceObserver(callback);

  1. 通过 observe 方法指定要观察的性能条目

这个方法支持三个参数:

1)entryTypes: 要观察的性能条目数组

2)type: string用于指定要观察的性能条目,不能同时和 entryTypes 一起使用。

目前支持的性能条目 大概有这些。

3)buffered: boolean, type配合使用,表示是否检查已缓冲的性能条目,为true时,将检查缓冲区的性能条目是否需要更新。

// 测量 LCP
new PerformanceObserver((entryList) => {}).observe({type: 'largest-contentful-paint', buffered: true});
// 测量 FID
new PerformanceObserver((entryList) => {}).observe({type: 'first-input', buffered: true});
// 测量 CLS
new PerformanceObserver((entryList) => {}).observe({type: 'layout-shift', buffered: true});
// 测量 FCP
new PerformanceObserver((entryList) => {}).observe({type: 'paint', buffered: true});
// 测量多个性能条目
new PerformanceObserver((entryList) => {}).observe({entryTypes: ['paint', 'largest-contentful-paint']});
  1. 获取性能指标测量结果

回调参数 entryList 为一个PerformanceEntryList,这个参数提供三个方法:getEntries()getEntriesByType()getEntriesByName()

new PerformanceObserver((entryList) => {
  // getEntries
  list.getEntries().forEach((entry) => {
    if (entry.entryType === "mark") {
      console.log(`${entry.name}'s startTime: ${entry.startTime}`);
    }
    if (entry.entryType === "measure") {
      console.log(`${entry.name}'s duration: ${entry.duration}`);
    }
  });
  // getEntriesByName
  for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
    console.log('FCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'paint', buffered: true});

每个 PerformanceEntry对象,都会包含以下字段信息:

interface PerformanceEntry {
  name: string
  entryType: string
  startTime: number
  duration: number
};

name: 主要取决于 PerformanceEntry.entryType的值,可参考

startTime: 取决于 PerformanceEntry.entryType, 不同类型startTime的取值不同。

duration:这个 performance 数据标识的持续时间,同样不同entryType取值不同,不适用duration的类型则始终返回0,如layout-shift

BTW: 还可以使用performance.mark()配合performance.measure()进行自定义埋点的测量。

performance API