Core Web Vitals
什么是 Google CWV
CWV(Core Web Vitals) 即核心 web 指标,当前侧重于用户体验的三个方面 —— 加载性能、交互性和视觉稳定性:
- Largest Contentful Paint (LCP) - 最大内容绘制,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的 2.5 秒内发生。
- First Input Delay (FID) - 首次输入延迟,测量交互性。为了提供良好的用户体验,页面的 FID 应为 100 毫秒或更短。
- Cumulative Layout Shift (CLS) - 累积布局偏移,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1 或更少。
LCP
诸如 load(加载)或 DOMContentLoaded(DOM 内容加载完毕)这样的旧有指标并不是很好,因为这些指标不一定与用户在屏幕上看到的内容相对应。而像 First Contentful Paint 首次内容绘制 (FCP) 这类以用户为中心的较新性能指标只会捕获加载体验最开始的部分。如果某个页面显示的是一段启动画面或加载指示,那么这些时刻与用户的关联性并不大。
LCP 指标会根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间。根据当前最大内容绘制 API 中的规定,最大内容绘制考量的元素类型为:
- <img>元素
- 内嵌在 <svg> 元素内的 <image> 元素
- <video> 元素(使用封面图像)
- 通过 url() 函数(而非使用 CSS 渐变)加载的带有背景图像的元素
- 包含文本节点或其他行内级文本元素子元素的块级元素。
如果有元素延伸到可视区域之外,或者任何元素被剪裁或包含不可见的溢出,则这些部分不计入元素大小。
我们可以从以上示例中看出,Instagram 标志加载得相对较早,即使其他内容随后陆续显示,但标志始终是最大元素。在 Instagram 时间轴的第一帧中,相机标志的周围没有用绿框框出。这是因为该标志是一个 <svg> 元素,而 <svg> 元素目前不被视为 LCP 候选对象。首个 LCP 候选对象是第二帧中的文本。
要在 JavaScript 中测量 LCP,可以使用最大内容绘制 API。以下示例说明了如何创建一个 PerformanceObserver 来侦听 largest-contentful-paint 条目并记录在控制台中:
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry)
}
}).observe({ type: 'largest-contentful-paint', buffered: true })
LCP 主要受四个因素影响:
- 缓慢的服务器响应速度 - 可通过 TTFB 来测量服务器响应时间
- JavaScript 和 CSS 渲染阻塞
- 资源加载时间
- 客户端渲染
改进方案,更多细节可以参考这里 👈:
- 使用 PRPL 模式做到即时加载
- 优化关键渲染路径
- 优化您的 CSS
- 优化您的图像
- 优化网页字体
- 优化您的 JavaScript(针对客户端渲染的网站)
FID
用户对您的网站加载速度的第一印象可以通过 FCP 进行测量。但网站在屏幕上绘制像素的速度只是其中一部分,同样重要的还有当用户试图与这些像素进行交互时,网站是否能够及时响应!
FID 测量从用户第一次与页面交互(例如当他们单击链接、点按按钮或使用由 JavaScript 驱动的自定义控件)直到浏览器对交互作出响应,并实际能够开始处理事件处理程序所经过的时间。为什么要测试首次?是因为第一印象至关重要。
FID 是测量页面加载期间响应度的指标。因此,FID 只关注不连续操作对应的输入事件,如点击、轻触和按键。其他诸如滚动和缩放之类的交互属于连续操作,具有完全不同的性能约束。
FID 是一个只能进行实际测量的指标。但是,Total Blocking Time 总阻塞时间 (TBT) 指标不仅可以进行实验室测量,还与实际的 FID 关联性强,而且可以捕获影响交互性的问题。能够在实验室中改进 TBT 的优化也应该能为您的用户改进 FID。
较长的首次输入延迟通常发生在 FCP 和 Time to Interactive 可交互时间 (TTI) 之间,因为在此期间,页面已经渲染出部分内容,但交互性还尚不可靠。根据以下时间轴可以看出,FCP 和 TTI 之间有相当长的一段时间(包括三段长任务),如果用户在这段时间内尝试与页面进行交互(例如单击一个链接),那么从浏览器接收到单击直至主线程能够响应之前就会有一段延迟:
要在 JavaScript 中测量 FID,您可以使用事件计时 API。以下示例说明了如何创建一个 PerformanceObserver 来侦听 first-input 条目并记录在控制台中:
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 })
改进方案:
- 减少第三方代码的影响
- 减少 JavaScript 执行时间
- 最小化主线程工作
- 保持较低的请求数和较小的传输大小
CLS
有没有遇到过一种情况,当你正要点击一个链接或一个按钮时,突然间移位了,结果点到了别的东西!顿时 C 语言!!页面内容的意外移动通常是由于异步加载资源,或者动态添加 DOM 元素到页面现有内容的上方。罪魁祸首可能是未知尺寸的图像或视频、实际渲染后比后备字体更大或更小的字体,或者是动态调整自身大小的第三方广告或小组件。
CLS 测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数。每当一个可见元素的位置从一个已渲染帧变更到下一个已渲染帧时,就发生了布局偏移。一连串的布局偏移,也叫会话窗口,是指一个或多个快速连续发生的单次布局偏移,每次偏移相隔的时间少于 1 秒,且整个窗口的最大持续时长为 5 秒。
布局偏移分数 = 影响分数 * 距离分数
影响分数:测量不稳定元素对两帧之间的可视区域产生的影响,即前一帧和当前帧的所有不稳定元素的可见区域集合:
在上图中,有一个元素在一帧中占据了一半的可视区域。接着,在下一帧中,元素下移了可视区域高度的 25%。红色虚线矩形框表示两帧中元素的可见区域集合,在本示例中,该集合占总可视区域的 75%,因此其影响分数为 0.75。
距离分数:测量不稳定元素相对于可视区域位移的距离(水平或垂直)。
还是以上图为准,最大的可视区域尺寸维度是高度,不稳定元素的位移距离为可视区域高度的 25%,因此距离分数为 0.25。因此在这个示例中,影响分数是 0.75 ,距离分数是 0.25 ,所以布局偏移分数是 0.75 * 0.25 = 0.1875
。
要在 JavaScript 中测量 CLS,您可以使用布局不稳定性 API。以下示例说明了如何创建一个 PerformanceObserver 来侦听意外 layout-shift 条目、将条目按会话分组、记录最大会话值,并在最大会话值发生改变时更新记录:
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 })
如何改进 CLS:
- 始终在您的图像和视频元素上包含尺寸属性,或者通过使用 CSS 长宽比容器之类的方式预留所需的空间 - 这种方法可以确保浏览器能够在加载图像期间在文档中分配正确的空间大小。请注意,您还可以使用 unsized-media 功能策略在支持功能策略的浏览器中强制执行此行为。
- 除非是对用户交互做出响应,否则切勿在现有内容的上方插入内容 - 这样能够确保发生的任何布局偏移都在预期之内。
- 首选转换动画,而不是触发布局偏移的属性动画 - 动画过渡的目标是提供状态与状态之间的上下文连续性。如 CSS transform 属性使您能够在不触发布局偏移的情况下为元素设置动画:
- 用
transform: scale()
来替代和调整 height 和 width 属性。 - 如需使元素能够四处移动,可以用
transform: translate()
来替代和调整 top、right、bottom 或 left 属性。
- 用
其他指标
FCP / FP
FCP(First Contentful Paint) 即首次内容绘制。测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,”内容”指的是文本、图像(包括背景图像)、<svg> 元素或非白色的 <canvas> 元素
。为了提供良好的用户体验,网站应该努力将首次内容绘制控制在 1.8 秒或以内。
要在 JavaScript 中测量 FCP,您可以使用绘制计时 API。以下示例说明了如何创建一个 PerformanceObserver 来侦听名称为 first-contentful-paint 的条目并记录在控制台中:
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:
- 消除阻塞渲染的资源
- 缩小 CSS
- 移除未使用的 CSS
- 预连接到所需的来源
- 减少服务器响应时间 (TTFB)
- 避免多个页面重定向
- 预加载关键请求
- 避免巨大的网络负载
- 使用高效的缓存策略服务静态资产
- 避免 DOM 过大
- 最小化关键请求深度
- 确保文本在网页字体加载期间保持可见
- 保持较低的请求数和较小的传输大小
FP 和 FCP 通常相同,但也可能是 FP 优先
FMP
FMP(First Meaningful Paint) 测量用户启动页面加载和页面呈现主要首屏内容之间的时间。当页面上呈现的第一位内容包括首屏内容时,FCP 和 FMP 通常是相同的。 但是,例如当 iframe 中的首屏有内容时,这些指标可能会有所不同。FMP 在 iframe 中的内容对用户可见时注册,而 FCP 不包括 iframe 内容,因此 FMP 可能打分更低。以下图片仅供参考:
TBT
TBT(Total Blocking Time) 即总阻塞时间。一个页面的总阻塞时间是在 FCP 和 TTI 之间发生的每个长任务的阻塞时间总和。每当出现长任务(在主线程上运行超过 50 毫秒的任务)时,主线程都被视作”阻塞状态”。我们说主线程处于”阻塞状态”是因为浏览器无法中断正在进行的任务。因此,如果用户在某个长任务运行期间与页面进行交互,那么浏览器必须等到任务完成后才能作出响应。
如上图,虽然在主线程上运行任务的总时间为 560 毫秒,但其中只有 345 毫秒被视为阻塞时间。
如何改进 TBT:
- 减少第三方代码的影响
- 减少 JavaScript 执行时间
- 最小化主线程工作
- 保持较低的请求数和较小的传输大小
TBT 指标应该在实验室中进行测量,与 FID、TTI 等强关联
TTI
TTI(Time to Interactive) 即可交互时间。测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。如需根据网页的性能跟踪计算 TTI,请执行以下步骤:
- 先进行 FCP。
- 沿时间轴正向搜索时长至少为 5 秒的安静窗口,其中,安静窗口的定义为:没有长任务且不超过两个正在处理的网络 GET 请求。
- 沿时间轴反向搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在 FCP 步骤停止执行。
- TTI 是安静窗口之前最后一个长任务的结束时间(如果没有找到长任务,则与 FCP 值相同)。
需要注意的是,SSR 等技术可能会导致页面看似具备交互性(即,链接和按钮在屏幕上可见),但实际上并不能进行交互,因为主线程被阻塞或是因为控制这些元素的 JavaScript 代码尚未完成加载。为了避免这个问题,请尽一切努力将 FCP 和 TTI 之间的差值降至最低。
如何改进 TTI:
- 缩小 JavaScript
- 预连接到所需的来源
- 预加载关键请求
- 减少第三方代码的影响
- 最小化关键请求深度
- 减少 JavaScript 执行时间
- 最小化主线程工作
- 保持较低的请求数和较小的传输大小
TTFB
TTFB(Time to First Byte) 即首字节时间。以下是网络请求阶段及其相关时序图。TTFB 测量 startTime 和 responseStart 之间经过的时间。
要在 JavaScript 中测量 TTFB,您可以使用绘制计时 API。以下示例说明了如何创建一个 PerformanceObserver 来侦听名称为 navigation 的条目并记录在控制台中:
new PerformanceObserver((entryList) => {
const [pageNav] = entryList.getEntriesByType('navigation')
console.log(`TTFB: ${pageNav.responseStart}`)
}).observe({
type: 'navigation',
buffered: true
})
如何改进 TTFB:
- Hosting services with inadequate infrastructure to handle high traffic loads
- Web servers with insufficient memory that can lead to thrashing
- Unoptimized database tables
- Suboptimal database server configuration
Speed Index
Speed Index 即首屏展现平均值,衡量内容在页面加载期间可视化显示的速度。PageSpeed Insights 使用以下分数对网站的首屏展现平均值进行排名并相应地对其进行颜色编码:
- 绿色(良好)— 0 到 3.4 秒
- 橙色(中等)— 3.4 到 5.8 秒
- 红色(慢) – 超过 5.8 秒
如何改进 Speed Index:
- 减少渲染阻塞资源 - 如延迟脚本执行
- 减少网站主线程工作 - 如优化第三方脚本,极简化 JS
- 确保在加载字体时,字体能够正常显示 - 如预加载字体
INP
Interaction to Next Paint(INP) 是 Google 2022.5 推出的新指标。是输入延迟(input delay)、处理时间(processing time)、呈现延迟(presentation delay) 3 个时间段的总和:
与 CLS 类似,INP 是在用户离开页面时计算的,产生一个代表页面在整个页面生命周期内的整体响应能力的值。如果高百分比页面交互得到快速响应,这意味着所有较低百分比的交互也很快。对应评分会取 TP75 的值,如下:
- 绿色(良好)— 0 到 200 毫秒
- 橙色(中等)— 200 到 500 毫秒
- 红色(慢) – 超过 500 毫秒
INP 涵盖了从页面开始加载到用户离开页面时可能发生的整个交互范围。通过对所有交互进行抽样,可以全面评估响应能力。这使得 INP 成为比 FID 更可靠的响应指标。
TP 指标
TP(Top Percentile) 指标指在一个时间段内,统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序, 并取出结果为:总次数 * 指标数 = 对应 TP 指标的值
,再取出排序好的时间。
如 TP50:指在一个时间段内,统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,取第 50% 的那个值作为 TP50 值;配置此监控指标对应的报警阀值后,需要保证在这个时间段内该方法所有调用的消耗时间至少有 50% 的值要小于此阀值,否则系统将会报警。TP90,TP99,TP999 同理,TP999 对方法性能要求很高。
举个例子,有四次请求耗时分别为:10ms,1000ms,100ms,2ms。计算 TP 非常简单:
- 将所有时间按升序排序:[2s,10s,100s,1000s]
- 在需要计算的部分中找到需要的项
- 对于 TP50,即 ceil(4 * 0.5)= 2,即需要第二个请求
- 对于 TP90,即 ceil(4 * 0.9)= 4,即需要第四个请求
- 我们在上面找到对应的时间,TP50 = 10s,TP90 = 1000s
什么是 PRPL
PRPL 是即时加载资源的一种策略:
- Push (or preload) the most important resources.
- Render the initial route as soon as possible.
- Pre-cache remaining assets.
- Lazy load other routes and non-critical assets.
一、Preload critical resources
关键字 preload 作为元素 <link> 的属性 rel 的值,代表需要浏览器预先获取和缓存对应资源。
<!-- The path to the resource in the href attribute. -->
<!-- The type of resource in the as attribute. -->
<link rel="preload" as="style" href="css/style.css">
二、Render the initial route as soon as possible
我们需要减少 First Paint,即第一个像素渲染到屏幕上所用的时间。目前的方案比如有使用 async/defer 来推迟 js 脚本执行时间,内联首屏使用的关键 CSS,或者 SSR。
三、Pre-cache assets
可以尝试使用 Service Worker 或者一些第三方库 Workbox 来制定缓存策略,可以不直接从服务器请求数据。
四、Lazy load
拆分较大的 chunks,实现按需加载。包括图片懒加载。
什么是关键渲染路径
浏览器将 HTML,CSS,JavaScript 转换为屏幕上所呈现的实际像素,这期间所经历的一系列步骤,就叫做关键渲染路径(Critical Rendering Path):
具体渲染步骤可以参考《页面渲染》这篇文章
如何去优化,可以围绕以下三点:
- 关键资源 - 可能阻止网页首次渲染的资源。关键资源的数量越少,浏览器处理渲染的工作量就越少,同时 CPU 及其他资源的占用也越少
- 关键路径长度 - 获取所有关键资源所需的往返次数或总时间
- 关键字节 - 实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。如压缩
整理出一些方案:
- HTML
- 减少 HTML 标签,避免不必要嵌套(减少 DOM 的深度),提高解析速度,降低重绘重排成本
- CSS
- 尽早,尽快下载,首屏只加载必要的样式文件,除去关键渲染路径中任何不必要的 CSS,避免阻塞渲染
- 减少 CSS 选择器层级(层级扁平),避免不必要的嵌套,降低选择器的复杂度,提高解析速度(BEM)
- JS
- 避免成为渲染阻塞的脚本(放置位置,加载位置非常重要)
- 首屏只加载必须的脚本文件,除去关键渲染路径中任何不必要的代码。对非必要的初始化逻辑代码和其他功能都应该延后
- 执行优化
更多优化细节可以参考这里 👈
什么是长任务
从用户的输入,再到显示器在视觉上给用户的输出,这一过程如果超过 100ms,那么用户会察觉到网页的卡顿,所以为了解决这个问题,每个任务不能超过 50ms,W3C 性能工作组在 LongTask 规范中也将超过 50ms 的任务定义为长任务。为了避免长任务,一种方案是使用 Web Worker,将长任务放在 Worker 线程中执行,缺点是无法访问 DOM,而另一种方案是使用时间切片。
时间切片的核心思想是:如果任务不能在 50 毫秒内执行完,那么为了不阻塞主线程,这个任务应该让出主线程的控制权,使浏览器可以处理其他任务。让出控制权意味着停止执行当前任务,让浏览器去执行其他任务,随后再回来继续执行没有执行完的任务。所以时间切片的目的是不阻塞主线程,而实现目的的技术手段是将一个长任务拆分成很多个不超过 50ms 的小任务分散在宏任务队列中执行。
我们可以模拟一个阻塞 1s 的长任务:
const start = performance.now()
while (performance.now() - start < 1000) {}
console.log('done!')
我们可以通过 performance 面板查看长任务情况,我们再实现一个 ts 方法如下:
function ts(gen) {
if (typeof gen === 'function') gen = gen()
if (!gen || typeof gen.next !== 'function') return
return function next() {
const start = performance.now()
let res = null
do {
res = gen.next()
} while(!res.done && performance.now() - start < 25) // 小于 50 即可
if (res.done) return
setTimeout(next)
}
}
ts(function* () {
const start = performance.now()
while (performance.now() - start < 1000) {
console.log(11)
yield
}
console.log('done!')
})()
通过切片我们可以看到一个长任务被切成了若干个小任务,在每个小任务间隔中把主线程的控制权交出来,这样就不会导致页面卡顿。代码核心思想:通过 yield 关键字可以将任务暂停执行,并让出主线程的控制权;通过 setTimeout 将未完成的任务重新放在任务队列中执行
performace 面板
详情可以查看官方文档 👈
首先我们最好在隐私窗口打开 Chrome,即使用 Incognito Mode
,避免一些不必要的影响。我们通过 Chrome DevTools 录制完后,会得到下面的面板数据:
官方提供的 demo 👈
时间线面板
- 多条对应的竖线代表着各项指标触发的时间,FP、FCP、DCL、onload 和 LCP,相同颜色的竖线例如 LCP 可能会多次触发,是一个计算的值,以最后一次的为准
- 红色部分代表帧率过低,多数由 JS 执行长任务导致 iframe 以及 services/web worker 的长任务也有可能导致 FPS 下降
- 黄色部分代表 JS 执行,紫色部分代表页面渲染
- 空白部分代表页面空闲
- 灰色斜杠部分代表 JS 主线程空闲,其它线程正在工作,此时可响应用户操作
网络请求面板
网络请求面板记录了整个页面运行期间发出的所有请求:
- 蓝色代表 HTML 请求
- 黄色代表 JS 请求
- 绿色代表图片请求
- 紫色代表 CSS 请求
- 灰色代表 AJAX 请求
- 深灰色代表字体文件请求
所有的请求都可能出现以下图中 4 种情况:
- 请求前空白阶段,在此阶段 JS 代码执行已经发出了 AJAX 请求,由于各种原因导致请求会延迟,如请求过多未推出浏览器请求队列(Queueing),网络阻塞(Stalled),DNS 查询(DNS Lookup),与服务器进行 TCP 连接(Initial connection),HTTPS 验证(SSL)
- 此阶段为 TTFB 耗时
- 从 2 阶段开始到 3 阶段开头,是浏览器接收到服务器发回来的第一个字段到接收结束的耗时。此阶段如果是 HTML,即使未完全接收整个 HTML,浏览器也会进行流式解析,执行 JS,CSS 并且下载页面需要的资源
- 不同的资源在第 4 阶段有些细微的不同
- Javascript,Img,CSS:主线程被占用,无法处理资源加载完毕时的 Finish Loading 事件,导致会多出这段时间来等待主线程处理
- ajax: 同样的主线程被占用,但是 ajax 的第四段会包含 4 处理此 ajax response 的 JS 代码执行时间,所以如果在 ajax 回调里处理大量数据时,如 commit Vuex,由于依赖较多,Vue 的 Get 耗时以及后续的 patchVNode 可能会形成长任务,影响 TTI,TBT 指标
线程面板
线程面板包含页面运行期间,浏览器在渲染执行页面的所有任务,例如光栅(Raster)线程,合成(Compositor)线程,Service Worker 线程等等,一般来说,我们只需要关注 Main 线程,来分析页面就可以了。主线程呈现形式为火焰图,记录浏览器运行时的堆栈信息,栈底在上,栈顶在下,可以详细的看到各个 Task 的执行顺序以及时间,配合时间线面板和请求面板可以分析代码运行时可能存在的问题:
其他的面包还包含:
- Summary - 显示线程面板中选定内容的详情,包含任务总耗时,任务时间占用情况环形图
- Bottom-Up - 通过点击 self time 进行排序可以查看花费了最多时间的任务,Activity 下为堆栈信
- Call Tree - 按执行量最多的任务进行显示,可以理解为默认 Total Time 排序
- Event Log - 在 Activity 单元格中按执行顺序显示
实验室工具 & 实测工具
实测工具:
- Chrome 用户体验报告
- PageSpeed Insights 网页速度测量工具
- 搜索控制台(核心 Web 指标报告)
- web-vitals JavaScript 库
实验室工具:
- Chrome 开发者工具
- Lighthouse
- WebPageTest 网页性能测试工具
参考文档
- web vitals by Philip Walton
- 优化关键渲染路径 by Berwin
- 时间切片 by Berwin