RAIL 性能模型
RAIL 是 Response、Animation、Idle 和 Load 的首字母缩写,是一种由 Google Chrome 团队于 2015 年提出的性能模型,用于提升浏览器内的用户体验和性能。
响应(Response):应该尽可能快速的响应用户,应该在 100ms 以内响应用户输入。所谓的响应并不是返回结果,**而是给出用户可以感知的状态交互。**例如击按钮向后台发起某项业务处理请求时,首先反馈给用户开始处理的提示,然后在处理完成的回调中反馈完成的提示。 动画(Animation) :在展示动画的时候,每一帧应该以 16ms 进行渲染,这样可以保持动画效果的一致性,并且避免卡顿,除去浏览器绘制新桢的时间,留给执行代码的时间仅 10ms 左右。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。此现象通常称为卡顿,会对用户体验产生负面影响。example:
googlechrome.github.io/devtools-sa…
空闲(Idle) :当使用 JavaScript 主线程的时候,应该把任务划分到执行时间小于 50ms 的片段中去,这样可以释放线程以进行用户交互。JS 中将超过 50ms 的执行任务称为**“长任务”**,由于长任务在执行过程中没有办法响应用户交互,所以要尽量避免 加载(Load) :应该在小于 1s 的时间内加载完成,并可以进行用户交互。用户感知要求我们尽量在 5s 内完成页面加载,如果没有完成,用户的注意力就会分散到其他事情上,并对当前处理的任务产生中断感。需要注意的是,这里在 5s 内完成加载并渲染出页面的要求,并非要完成所有页面资源的加载,从用户感知体验的角度来说,只要关键渲染路径完成,用户就会认为全部加载已完成。
基于用户体验的核心指标
FCP
基本概念
-
First contentful paint 首次内容绘制 (FCP):页面开始加载到页面显示任何内容的时间。 首次绘制DOM内容的时间,内容必须是文本、图片、非白色的canvas或svg等。这个指标通常会被用来衡量白屏时间。
-
速度指标
测量方法
- 使用 Performance API
window.addEventListener('load', () => {
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
this.fcpTime1 = entry.startTime;
}
});
});
- 存在问题:时序性问题,这个API返回的是已经记录的性能条目,但如果过早地调用它,例如在DOMContentLoaded事件处理函数中,可能无法获取到FCP的值,因为此时FCP可能还没有发生。
- 解决方法: performanceObserve
-
使用PerformanceObserver测量FCP
PerformancePaintTiming
接口提供有关网页构建期间“绘制”(也称为“渲染”)操作的计时信息。“绘制”是指将渲染树转换为屏幕上的像素。
measureFCP() {
const performanceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name === 'first-contentful-paint') {
this.fcpTime = entry.startTime;
}
});
});
performanceObserver.observe({ type: 'paint', buffered: true });
},
当使用performance.getEntriesByType('paint')
时,只能获取到那些在调用这个函数时已经发生的paint事件,这就意味着必须在合适的时机调用这个函数,以确保能获取到FCP事件。
而使用PerformanceObserver来使用Paint Timing API时,创建了一个observer,让它在FCP事件发生时自动运行回调函数。不需要关心调用时机的问题,浏览器会在合适的时机自动调用回调函数。因此,使用PerformanceObserver的方式更加灵活,也更不容易出错。
-
存在问题
-
该 API 会为在后台标签页中加载的网页分派
first-contentful-paint
条目,但在计算 FCP 时应忽略这些网页(只有当网页始终位于前台时,才应考虑首次绘制时间)。 -
网页从往返缓存(BFCache)中恢复时,该 API 不会报告
first-contentful-paint
条目;但在这种情况下,应衡量 FCP,因为用户将它们视为不同的网页访问。BFCache:当我们从一个页面导航到另一个页面时,浏览器将保存上一个页面在BFCache中,当返回第一个页面的时候,浏览器直接从BFCache中进行页面恢复,不会重新加载和渲染。浏览器不会重新触发paint事件。
只有特定的浏览器会出现这个问题,如火狐浏览器,谷歌浏览器不具备。demo地址
-
API 可能无法报告来自跨源 iframe 的绘制时间。如果一个网页中包含了一个跨源的iFrame,那么这个网页通常无法获取到这个iFrame的绘制时间。这是因为iFrame的绘制时间是由iFrame内部的页面决定的,而不是由包含它的页面决定的
以上这些问题在其他的指标计算中也会存在。
-
-
解决方案:
用
web-vitals
JavaScript 库来衡量 FCP。
- 使用 web_vitals
onFCP((metric) => {
this.fcpTime2 = metric.value;
});
web_vitals本质上也是使用PerformanceObserver API,但做了一些优化。例如,
-
处理 BFCache 恢复,页面从BFCache中恢复时,浏览器会触发一个
pageshow
事件,Web Vitals库会监听这个事件,库就知道页面是从BFCache中恢复的,然后它会重新初始化所有的性能指标,以准备重新进行测量。 -
处理页面隐藏:
onFCP
内置了 Visibility API 的检查,以确保只有在页面可见时才记录性能数据。这对于处理页面被切换到另一个标签页或隐藏在背后时非常重要。 -
使用工具如:PageSpeed Insights、lighthouse等
影响因素和优化方案
影响FCP的元素有很多,例如堵塞渲染的资源(js、css) 、网页负载过大、DOM规模太大以及TTFB时间太长等。解决方案包括优化服务器性能、减少JavaScript的使用、减小资源文件的大小、使用CDN等。在确保关键资源优先加载并尽快渲染的同时,尽可能地推迟加载非关键资源。下面我们以堵塞的资源举例
<script>
// 模拟一个耗时的JavaScript操作
function timeConsumingOperation() {
for (let i = 0; i < 1000000000; i++) {
}
}
// 调用一个耗时的JavaScript函数
timeConsumingOperation();
// 修改DOM,将新文本插入到页面中
const newElement = document.createElement('p');
newElement.textContent = 'This is added by JavaScript';
document.body.appendChild(newElement);
</script>
改成异步调用,避免堵塞。
<script>
// 异步函数来执行耗时的JavaScript操作
async function asyncTimeConsumingOperation() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
}
// 调用异步的JavaScript函数
asyncTimeConsumingOperation().then(() => {
// 修改DOM,将新文本插入到页面中
const newElement = document.createElement("p");
newElement.textContent = "This is added by JavaScript";
document.body.appendChild(newElement);
});
</script>
TTFB
基本概念
Time To First Byte,即首字节时间,用于测量资源请求与响应的第一个字节开始到达之间的时间。TTFB会测量StartTime到responseStart之间经过的时间。
关于 TTFB 的一个常见误解是它与服务器响应时间相同。然而,服务器响应时间衡量的是服务器响应的速度,而不是响应到达客户端的速度。它的测量中不包括网络延迟,这也是影响真实用户体验的因素。
-
指标
800到 1800 毫秒的范围相当平均,尤其是对于动态内容。 超过 1800 毫秒的时间 通常都表明服务器端存在问题,尤其是在靠近服务器的位置 TTFB 较高时。
测量方法
-
cURL
curl -o /dev/null -w "TTFB: %{time_starttransfer}\n"
http://localhost:8080
-
使用 PerformanceObserver的Navigation Timing API 在浏览器中衡量导航请求的 TTFB
const performanceObserver = new PerformanceObserver((entryList) => {
const [pageNav] = entryList.getEntriesByType('navigation');
this.ttfb1 = pageNav.responseStart;
})
performanceObserver.observe({ type: 'navigation', buffered: true });
- web-vitals
onTTFB((metric) => {
this.ttfb2 = metric.value;
});
影响因素和优化方案
影响因素
- 请求到达服务器的时间花费:当访问一个网站时,浏览器会向服务器发送HTTP请求。在这个阶段会有很多因素导致延迟,例如DNS查找速度慢、物理距离以及客户端的网络速度等。
- 服务器处理请求花费的时间:一旦请求到达目的地,服务器必须处理它并生成响应。可能得延迟因素有:数据库调用缓慢、缺乏缓存层、必须在服务器上执行的脚本过多、服务器资源不足等。
- 第一个字节到达客户端花费的时间:这里造成延迟的主要原因是服务器和客户端的网速慢。
总的来说TTFB很大程度上会受到终端用户自身环境的影响。此外还有一些因素会对TTFB的分数造成影响。
优化方案
- 缓存;
- 内置CDN;
- 完整的图像优化;
- HTML、CSS 和 JS 缩小和压缩;
- 关键 CSS、DNS 预取、预加载等。
LCP
基本概念
Largest contentful paint 最大内容绘制,页面开始加载到最大文本块或图像元素在屏幕上完成渲染的时间。
- 速度指标:
-
考虑的元素:
<img>
元素<svg>
元素内的<image>
元素- 包含海报图片的
<video>
元素(系统会使用海报图片加载时间) - 带有
url()
函数加载的背景图片的元素 - 包含文本节点或其他内嵌级别文本元素的子项的块级元素
- 自动播放
<video>
元素而绘制的第一帧 - 动画图片格式的第一帧
除了考虑元素之外,还有一些小方法来排除"非内容"的元素。例如
-
不透明度为 0 且对用户不可见的元素
-
覆盖整个视口的元素,很可能被视为背景而非内容
-
占位符图片或其他低熵的图片,可能无法反映网页真实内容
元素的大小:
尺寸通常是用户在视口中可见的尺寸。如果该元素延伸到视口之外、被剪裁或不可见的overflow,都不会计入元素的大小。
对于已根据其固有尺寸调整大小的图片元素,报告的尺寸是可见尺寸或固有尺寸(以较小者为准)
对于文本元素,系统仅考虑文本节点的大小(包含所有文本节点的最小矩形)。
对于所有元素,不考虑通过 CSS 应用的任何外边距、内边距或边框。
网页通常会分阶段加载,因此网页上最大的元素可能会发生变化。
上报时机:
为了应对这种可能的变化,浏览器在绘制完第一帧后,会立即分派 largest-contentful-paint
类型的 PerformanceEntry
,用于标识最大的内容元素。在渲染后续帧后,只要最大内容元素发生变化,该 API 就会再分派另一个 PerformanceEntry
。总的来说就是会进行更新。
请务必注意,元素只有在呈现并对用户可见后,才能被视为最大的内容元素。尚未加载的图片不会被视为“已渲染”。在字体块期间,使用网页字体的文本节点也不例外。
停止时机:
一旦用户与页面交互(通过点按、滚动或按键),浏览器就会停止报告新条目。因此最近分派的 PerformanceEntry
是比较具备分析意义的。
如图点击交互之后不上报两面之后加载的图片:
在某些情况下,网页上最重要的元素与最大的元素不同,开发者可能更希望衡量其他这些元素的呈现时间。这可以使用 Element Timing API 实现。
-
demo
通过Element Timing API指定上报内容,可以对之前忽略的文本内容进行上报
<template>
<div>
<img elementtiming="image" src="../../assets/生气 (1).png" />
<p elementtiming="title" id="title">This is text I care about.</p>
</div>
</template>
<script>
export default {
data() {
return {
displayHeader: true,
displayImg: false,
displayBlock: false,
};
},
mounted() {
this.getLCP();
this.getCustomLCP();
},
methods: {
getLCP() {
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log("LCP candidate:", entry.startTime, entry);
}
}).observe({ type: "largest-contentful-paint", buffered: true });
},
getCustomLCP() {
const po = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('custom 元素',entry.toJSON());
}
});
po.observe({ type: "element", buffered: true });
},
},
};
</script>
<style scoped>
.one {
width: 150px;
height: 150px;
color: #fff;
background-color: red;
}
.two {
width: 80px;
height: 80px;
color: #fff;
background-color: blue;
}
.large-block {
width: 500px;
height: 500px;
backgroud-image: url("../../assets/生气 (1).png");
}
</style>
测量方法
- Largest Contentful Paint API
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry);
}
}).observe({type: 'largest-contentful-paint', buffered: true});
存在的问题:
-
在页面已转入后台运行或后台标签页中加载页面时,会继续分派
largest-contentful-paint
条目,但在计算 LCP 时应忽略这些条目(只有当页面始终位于前台时,才应考虑相关元素)。 -
从BFCache中恢复网页时,API 不会报告
largest-contentful-paint
条目。 -
不会考虑 iframe 中的元素。
-
web-vitals
onLCP((metric) => {
this.lcp1 = metric.value;
})
-
工具
影响因素和优化方案
LCP可以划分成几个小部分
LCP 子部分 | 说明 |
加载第一个字节所需时间 (TTFB) | 从用户开始加载网页到浏览器收到 HTML 文档响应的第一个字节的时间。 |
资源加载延迟 | TTFB 与浏览器开始加载 LCP 资源之间的增量。 |
资源加载时间 | 加载 LCP 资源本身所需的时间。 |
渲染延迟 | LCP资源完成加载到LCP元素完全渲染之间的时间 |
可以受到许多因素的影响,包括:服务器响应时间、静态资源大小、客户端渲染可能导致额外的JavaScript执行时间、同步加载的JavaScript或CSS,它们可能会阻塞页面渲染,从而影响LCP。
下面我们以大图片为例,会发现最初LCP为左侧文字部分,随后加载过程中变成中间大图片。且LCP值较大。当开启了缓存,LCP会直接拿到缓存中的大图片,且LCP值较小
TTI
前置概念
长任务
一个任务是浏览器执行的任何**「离散工作单元」**
主要包括
- 渲染、解析HTML和CSS
- 运行JavaScript代码
- 以及其他一些可能无法直接控制的工作
其中,「JavaScript是主要的任务来源之一」
「主线程一次只能处理一个任务」。当任务的执行时间超过一定阈值(50毫秒),它们被归类为长任务。下图为在performance观测的长任务:【演示】
如果用户在浏览器执行长任务的时候与页面进行交互,或者重新渲染,浏览器将延迟处理这些工作,这导致交互或渲染延迟。
为了更好地执行任务,就可以进行任务拆分。这意味着将一个长任务分割成较小的任务,使它们在单独运行时所需的时间更短。 当任务被拆分时,浏览器有更多机会**「响应更高优先级」**的工作,其中包括用户的交互操作。
页面完全可交互
"页面完全可交互"(Page Fully Interactive
)是指在网页加载完成后,**「所有」**主要的用户交互元素和功能都已经加载并且可以响应用户的操作,用户可以在页面上执行各种操作而不会出现明显的延迟或等待。当页面完全可交互时,用户体验更加流畅,因为用户可以立即与页面进行交互,无需等待页面响应。
当一个网页达到页面完全可交互
的状态时,以下几个条件应当满足:
- 「页面结构已经完全加载:」 所有HTML文档、CSS样式表和JavaScript脚本都已下载完成,并且浏览器已经解析和构建了整个页面的DOM结构。
- 「主要内容可见:」 网页的主要内容已经在浏览器窗口中可见,用户可以看到页面的核心信息而不需要进行滚动或等待。
- 「用户交互元素可用:」 网页上的按钮、链接、表单等交互元素都已加载并且可以正常响应用户的点击、输入或其他操作。
- 「动态功能可用:」 如果网页中有使用JavaScript实现的动态功能,如下拉菜单、轮播图等,这些功能也已经可以正常使用。
- 「响应时间短:」 当用户执行交互操作时,网页的响应时间非常短,用户几乎感觉不到延迟。例如,点击按钮立即有响应,页面切换或动画流畅运行。
基本概念
Time to Interactive 可交互时间,页面开始加载到能够快速、可靠地响应用户输入所需的时间。 以此测量页面的有效性、可交互性。
计算规则
TTI
的计量单位是秒**,计算页面的TTI
需要识别FCP
和首个5秒静默窗口**(在这个窗口中,浏览器不应处理主线程上的长任务,也不应等待超过两个服务器响应请求)
TTI
是FCP
和静默窗口之间的一个时间点,它恰好位于静默窗口之前的最后一个长任务完成时。
得分情况
影响因素和优化方案
影响因素
FCP到长任务结束的时间,加快网络请求的时间
优化方案
如需了解如何从总体上改进 TTI(针对任何网站),请参阅以下性能指南:
- 缩减 JavaScript 大小
- 预先连接到所需的源
- 预加载密钥请求
- 降低第三方代码的影响
- 最大限度地降低关键请求深度
- 缩短 JavaScript 执行时间
- 最大限度地减少主线程工作
- 尽量减少请求数和传输大小
CLS
在我们浏览网页时,有时会遇到下面的情况:
这种场景,一般由于
- 资源是以异步方式加载的,或 DOM 元素被动态添加到页面中现有内容的上方,通常就会发生意外的网页内容移动。问题可能是:尺寸未知的图片或视频、呈现大于或小于其后备图片的字体,
- 会动态调整自身大小的第三方广告或软件。
都会带来不有好的用户体验。
基本概念
CLS就是用来测量这些元素突变的。
Cumulative layout shift 累积布局位移 ,页面在整个生命周期内元素发生的所有布局偏移的分数总和。
爆发布局偏移称为会话窗口,是指快速连续发生一个或多个布局偏移,且每次偏移之间间隔不到 1 秒,且总时长不超过 5 秒。
最大突发是该窗口内所有布局偏移的累计得分最高的会话窗口。
会话窗口示例。蓝条表示每次布局偏移的分数。
得分情况
为了提供良好的用户体验,网站应尽力将 CLS 得分保持在 0.1 或更低。
计算规则
为了计算布局偏移分数,浏览器会查看视口尺寸以及视口中不稳定元素在两个渲染帧之间的移动情况。布局偏移分数是这种移动的两种衡量指标的乘积:影响分数和距离分数(两者都在下面定义)。
layout shift score = impact fraction * distance fraction
影响分数
可衡量不稳定的元素如何影响两帧之间的视口区域。
上一帧的所有不稳定元素与当前帧的可见区域(占视口总面积的比例)的并集就是当前帧的影响比例。
在上图中,一个元素占据了一帧的一半视口。然后,在下一帧中,该元素会向下移动 25% 的视口高度。红色虚线矩形表示元素在两个帧中可见区域的并集,在本例中占总视口的 75%,因此其_影响分数_为 0.75
。
距离分数
用于测量不稳定元素相对于视口的移动距离。
距离比例是任何不稳定元素在框架内的移动距离(水平或垂直方向)的最大距离除以视口的最大尺寸(宽度或高度,以较大者为准)。
在上例中,最大的视口尺寸为高度,不稳定的元素移动了视口高度的 25%,使得距离比例为 0.25。
因此,在此示例中,影响比例 为
0.75
, 距离比例 为0.25
,因此 布局偏移分数CLS为0.75 * 0.25 = 0.1875
。
预期与意外的布局偏移
并非所有布局偏移都是糟糕的。事实上,许多动态 Web 应用经常会更改页面上元素的初始位置。
只有在出乎用户意料时,布局偏移才会造成不良影响。另一方面,为了响应用户互动(点击链接、按下按钮、在搜索框中输入内容等)而发生的布局偏移是可以接受的.
测量方法
1,通过PerformanceObserver测量
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('Layout shift:', entry);
}
}).observe({type: 'layout-shift', buffered: true});
【absolute演示】
2,使用lighthouse
影响因素和优化方案
- 没有尺寸的图片、视频、iframe或其他延迟加载的内容
【CLS演示】
<video controls width={1920} height={1080}>
<source src='https://web.dev/static/articles/optimize-cls/video/tcFciHGuF3MxnTr1y5ue01OGLBn2/10TEOBGBqZm1SEXE7KiC.webm?hl=zh-cn'></source>
</video>
<video controls>
<source src='https://web.dev/static/articles/optimize-cls/video/tcFciHGuF3MxnTr1y5ue01OGLBn2/10TEOBGBqZm1SEXE7KiC.webm?hl=zh-cn'></source>
</video>
所以在使用图片的时候,尽量给图片或视频设置一个初始宽高,确保浏览器能够在内容加载期间在文档中分配正确的空间量,从而减小CLS。
- 动画
对 CSS 属性值的更改可能需要浏览器对这些更改做出反应。有很多值会触发重新布局和绘制,例如 box-shadow
和 box-sizing
。所以在开发中应尽量避免为这些元素添加动画效果。
而是使用更高效的方式来改变 CSS 属性例如,transform
动画可用于平移、缩放、旋转或倾斜,而不会触发重新布局,从而完全避免布局偏移。
如需详细了解高性能动画的做法,请参阅高性能动画