性能优化之详解各种指标

2023/7/10 性能

# 前言

上篇文章最后提到了我们可以通过performance的一些属性对性能做统计,我们会发现performance对象下有非常多的属性,远不止上篇文章提到的DOMContentLoadedLoad这两个事件。

或许你在浏览器控制台见过它们这些身影:DCLLCPFPFCPL

load2-1.png

这里的DCLL就是我们上篇文章介绍的DOMContentLoadedLoad这两个事件,那剩下的LCPFPFCP又分别代表什么呢?

在早期前端三剑客的时代或者现在的服务端渲染,DCLL确实可以很好地衡量首屏内容展示时间,但对于现代各种框架盛行的单页应用,由于都是通过JS操作DOM向页面添加主要内容,DCLL事件就不能很好地衡量首屏显示时间了。

于是有FP、FCP、FMP被提出来,它们关注的不是加载,而是渲染,因此能更好地表现用户看到的情况。

FP、FCP这两个指标虽然表达了渲染的事件,但对“用户关注的内容”没有体现,比如首屏渲染出来一个背景,或者一个loading,可能对于用户来说和白屏区别不大。FMP虽然体现了“关键内容”的要素,但它是复杂的、模糊的,甚至是错误的,并不能准确识别页面主要内容的加载时机。

后来LCP指标被提出来,表示“用于度量视口中最大的内容元素何时可见”,它用来代替FMP,表征页面的关键元素何时可以被用户看到。

除了加载性能,还有可交互时间、稳定性指标、流畅性指标,在不同的业务场景都可以被监控用来作为提升用户体验的依据。

# 性能相关

上面我们提到了各种性能相关的事件,那么它们各种代表的含义是什么呢?

# 关键事件

名词 全称 解释
FP firstPaint 首次绘制
FCP firstContentfulPaint 首次内容绘制
LCP largestContentfulPaint 最大内容绘制
DCL domContentLoaded dom内容解析完成
L loaded 页面的load事件

# DCL(DOMContentLoaded)

当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。

# 事件监听

document.addEventListener('DOMContentLoaded', (event) => {
    console.log('DOM 完全加载以及解析')
});

# 耗时计算

performance.timing.domContentLoadedEventStart - performance.timing.fetchStart

# Load

load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与 DOMContentLoaded (opens new window) 不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载。

# 事件监听

window.addEventListener('load', (event) => {
  console.log('页面加载完成');
});

# 耗时计算

performance.timing.loadEventEnd - performance.timing.fetchStart

# DCL与Load触发的先后顺序

很多人可能会误以为Load的触发一定会在DCL之后,虽然绝大多数我们看到的确实是这样,但你从两者的MDN解释上来看,DCL关注的时HTML文档的加载与解析,而Load只关注资源的加载,所以两者触发的先后顺序并不是绝对的。

比如下面两种情况:

第一种: 页面非常简单,没有引入任何外部资源

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        
    </div>

</body>
</html>

load2-2.png

从图中可以看到此时的Load触发在DCL之前,这是因为load不包含对文档解析的时间

第二种: 我们引入一张图片

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        <img src="https://imgservices-1252317822.image.myqcloud.com/coco/s06012023/9ac85415.g0q5wz.png" class="zan_icon" />
    </div>

</body>
</html>

load2-3.png

从这张图上我们可以看到此时DCLLoad先触发,并且很明显可以看到在两者之前多了个图片的下载过程。

所以两者触发的先后顺序并不是固定的,如果页面有许多外部资源需要加载,那么load 事件会后触发,如果页面内容较多,外部资源较少,那么load事件可能先触发。

为了应对现在框架盛行的单页应用新增了下面这几个指标,它们关注的不再是页面的加载过程,而是页面的渲染过程。

# FP与FCP

为了方便,这两个放一起讲:

FP,全称 First Paint, 代表首次渲染的时间点,即首次视觉变化发生的时间点。前端开发者经常谈到的白屏时间(用户看不到任何内容)就是用户访问网页到 FP 的这段时间

FCP,全称 First Contentful Paint,代表首次 DOM 内容 渲染的时间点DOM 内容 可以是文本、图像(包括背景图像)、<svg>元素或非白色的 <canvas> 元素。

简单点理解就是FCP事件指渲染出第一个内容的事件,而FP指渲染出第一个像素点,渲染出的东西可能是内容,也可能不是。

⚠️需要注意的是,FP一定不会比FCP晚触发,但可能会一起触发,绝大多数情况是FP在FCP之前触发!

# 几种场景

第一种: 无FP

是不是很奇怪,怎么会有这种情况?其实我们上面DCLLoad那里的第一个案例就出现了这种情况

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        
    </div>

</body>
</html>

load2-2.png

页面上有节点但没有样式,很显然这种情况是不需要渲染页面的,所以也就没有FP

当然这里需要注意的是这里的节点不包括一些自身可见的节点(比如img、input、video等)

还有一种情况就是如果要渲染的内容在视口之外,那么也不会触发 FP

第二种: 有FP无FCP

同时为了验证第一种说法的注意点,这里我就写一个input来试试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        <input type="text" />
    </div>

</body>
</html>

load2-4.png

从这里我们就能发现,此时页面只会触发FP,因为没有内容

# 耗时计算

// FP
const fp = performance.getEntries('paint').filter(entry => entry.name == 'first-paint')[0].startTime;

// FCP
const fcp = performance.getEntries('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime;

# FP与DCL的触发顺序

浏览器不一定等到所有的DOM都解析完再开始渲染,如果DOM节点少,浏览器会加载完再渲染,但是如果节点很多,浏览器解析一部分节点后就会开始渲染(这时候就会触发FP)。也就是说,当需要渲染的节点数少的时候,DCL会在FP前面;当需要渲染的节点数很多时候,DCL会在FP后面。

现在来说,绝大部分的项目都是FPDCL之前触发,这样用户可以更快的看到页面内容。

# LCP

LCP,全称 Largest Contentful Paint,根据页面首次开始加载的时间点(即 first started loading,可以通过 performance.timeOrigin 得到)来报告可视区域内可见的最大图像或文本块完成渲染的相对时间

# LCP评分

为了提供良好的用户体验,我们应努力将最大内容绘制时间控制在2.5 秒或更短。

load2-5.png

# LCP包含哪些元素类型

  • <img>元素
  • <image><svg>元素内的元素
  • <video>带有海报图像的元素(使用海报图像加载时间)
  • 具有通过函数加载的背景图像的元素url()(而不是CSS 渐变)
  • 包含文本节点或其他内联级文本元素子元素的块级元素。

随着更多研究的进行,未来可能会添加其他元素

# 如何确定页面的LCP元素

这里可以通过performance面板,在Timings这一行找到LCP,点击它再找到下面的summary,就能找到LCP对应的节点元素了。

load2-6.png

# 耗时计算

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

# 体验相关

谷歌一直十分重视网站的用户体验,移动友好性,页面加载速度和HTTPS是Google已经使用的页面排名因素,而2020年,谷歌将Core Web Vitals新纳入的用户体验指标。

# TTI

TTI 全称Time to Interactive 它用于测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。

# 测量步骤

测量TTI一般需要按以下步骤:

  1. 先进行首次内容绘制 (FCP)
  2. 沿时间轴正向搜索时长至少为 5 秒的安静窗口,其中,安静窗口的定义为:没有long task且不超过两个正在处理的网络 GET 请求
  3. 沿时间轴反向搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在 FCP 步骤停止执行
  4. TTI 是安静窗口之前最后一个长任务的结束时间(如果没有找到长任务,则与 FCP 值相同)

为了方便理解,可以对照这张图

load2-7.png

# 优秀指标

为了提供良好的用户体验,网站在普通移动硬件上进行测试时,应该努力将可交互时间控制在5 秒以内。

# TBT

TBT全称 Total Blocking Time, 它主要是用于度量 FCP 和 TTI 之间的总的阻塞时间。

只要存在long task(在主线程上运行超过 50 毫秒 (ms) 的任务),主线程就会被视为“阻塞”。我们说主线程被“阻塞”,因为浏览器无法中断正在进行的任务。因此,如果用户在长时间任务中确实与页面进行交互,则浏览器必须等待任务完成才能响应*。*

# 优秀指标

测量TBT可以使用谷歌Lighthouse

为了提供良好的用户体验,在平均移动硬件上进行测试时,网站应努力使总阻塞时间低于200 毫秒

# CLS

CLS 全称 Cumulative Layout Shift 累积布局偏移 ,它用来测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数

# CLS详情

只要可视区域中可见元素的起始位置(例如,元素在默认书写模式下的顶部和左侧位置)在两帧之间发生了变更,该 API 就会报告layout-shift条目。这样的元素被视为不稳定元素

请注意,只有当现有元素的起始位置发生变更时才算作布局偏移。如果将新元素添加到 DOM 或是现有元素更改大小,则不算作布局偏移,前提是元素的变更不会导致其他可见元素的起始位置发生改变。

# CLS分数

浏览器在计算布局偏移分数时,会查看可视区域大小和两个已渲染帧之间的可视区域中不稳定元素的位移。布局偏移分数是该位移的两个度量的乘积:影响分数距离分数(两者定义如下)。

布局偏移分数 = 影响分数 * 距离分数

# 优秀指标

为了提供良好的用户体验,网站应该努力将 CLS 分数控制在0.1 或以下。为了确保您能够在大部分用户的访问期间达成建议目标值,一个良好的测量阈值为页面加载的第 75 个百分位数,且该阈值同时适用于移动和桌面设备。

这里需要注意的是在写动画时优先考虑使用CSS transform属性,因为它能够在不触发布局偏移的情况下为元素设置动画:

  • transform: scale()来替代和调整heightwidth属性。
  • 如需使元素能够四处移动,可以用transform: translate()来替代和调整toprightbottomleft属性。

# web-vitals

为了更准确的统计性能数据,可以使用web-vitals库来测量各项性能指标

可以获取的指标有:CLS、FID、LCP、以及 FCP、TTFB

import {getCLS, getFID, getLCP} from 'web-vitals'

getCLS(console.log);
getFID(console.log);
getLCP(console.log);

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~