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

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

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

网站的三大性能指标

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

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

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

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

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 秒之间。

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

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

关于元素的大小

  • LCP 仅考虑用户看得到的部分例如,一个元素有部分内容是在视口之外,这些部分不会计入元素的大小。对于图片元素,如果是缩小其原始大小,则以显示时的大小为准;如果是拉伸其尺寸,则以其原始大小为准。

  • 对于所有元素,不考虑通过 CSS 添加的任何外边距、内边距或边框。

Largest Contentful Paint 的选择

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

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

使用 JavaScript 测量 LCP

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

js 复制代码
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。

FID:首次输入延迟

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

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

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

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 面板运行。

js 复制代码
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。

CLS:累积布局偏移

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

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

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

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

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

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

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

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

使用 JavaScript 测量 CLS

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

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

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

Performance 面板

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

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

其他

DCL: DOMContentLoaded

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

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

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

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

  • defer: 与默认情况类似,是在 GUI 线程和 JavaScript 引擎线程执行完成,才会触发DOMContentLoaded
  • async 在 DOM 树构建完毕就会触发 DOMContentLoaded

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

js 复制代码
// 情况一
<script src="./main.js"></script>
<script src="http://localhost:3000/main.js" defer></script>
<script src="http://localhost:3000/test.js"></script>
js 复制代码
// 情况二
<script src="./main.js"></script>
<script src="http://localhost:3000/main.js" async></script>

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

FCP:首次内容渲染

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

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

使用 JavaScript 测量 FCP

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

js 复制代码
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。

其他:FP 和 Load

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

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


参考资料

Core Web Vitals Technology Report

Web Metrics

浏览器加载

相关推荐
anOnion4 小时前
构建无障碍组件之Menu Button pattern
前端·html·交互设计
用户47949283569154 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
zhangxingchao7 小时前
Kotlin常用的Flow 操作符整理
前端
IT_陈寒8 小时前
React的useState居然还有这种坑?我差点删库跑路
前端·人工智能·后端
Pedantic9 小时前
SwiftUI 手势笔记
前端·后端
橙子家10 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user205855615181310 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州10 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic12 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端