网页性能进阶:通过 performanceObserver 掌控关键指标

438 阅读11分钟

谈到 性能,初学者们都支支吾吾的说不出来点什么,都不知道怎么去真正的优化或者去监测,总觉得这是一个很高深的东西,例如 FCP LCP FID CLS TTFB 这些专性能指标词汇大部分人都只是简单的听过,都不知道其实际的含义,接下来会带着大家从性能中去了解并监测这些指标。

以前没有一些性能的API的时候,开发者们大部分通过开发者工具(以谷歌浏览器为例) Performance 选项卡的时间线面板来进行性能瓶颈分析或者有一些通过在项目中的一些关键性节点嵌入代码进行log记录,在效率和准确性上有待提高。

PerformanceObserver()

这个时候PerformanceObserver API横空出世了,它可以精准的记录一些性能指标,可以有效的帮助我们去针对性的优化项目。并且它是基于事件驱动的异步监测方式,不会阻塞主线程的执行。

我们直接切入主题,看看这个API可以监测哪些性能数据(带 * 是比较常用的性能指标的条目)

entryType(性能检测条目)功能
element观察与特定 DOM 元素相关的性能事件
event观察 JavaScript 事件处理相关的性能
* first-input记录用户在页面上的首次输入相关的性能
* largest-contentful-paint测量页面上最大内容元素(如最大的图片、文本块等)的渲染时间
* layout-shift记录了页面上非预期的位移波动
long-animation-frame观察长时间的动画帧相关性能
long-task用于检测长时间运行的任务
* navigation记录页面导航性能,包括 DNS 查询、TCP 连接、TTFB(首字节时间)等关键时间点。
resource记录页面加载的所有静态资源(如 CSS、JS、图片、字体等)的加载性能。
mark记录开发者定义的时间点(标记)。
measure记录开发者定义的代码执行时间(测量)。
* paint记录页面的渲染性能,包括 first-paintfirst-contentful-paint
taskattribution对长任务有重大影响的工作类型
visibility-state观察页面的可见性状态变化

我们也可以通过 PerformanceObserver.supportedEntryTypes 来查看浏览器支持哪些性能条目,下面是我谷歌浏览器所输出的 entryTypes

support-entrytype.png

语法

var observer = new PerformanceObserver(callback);

调用回调时,其第一个参数是性能观察条目列表,第二个参数是观察者对象。

callback参数

观察的性能事件被记录时将调用 PerformanceObserverCallback 回调

实例方法

disconnect()

用于阻止性能观察者接收任何性能条目事件。

observe()

用于观察传入的参数中指定的性能条目类型的集合。

takeRecords()

返回当前存储在性能观察器中的性能条目列表,并将其清空。

完整例子

var observer = new PerformanceObserver(function (list) {
  for (const entry of entryList.getEntries()) { // 获取对应监听到的条目集合
    console.log(entry);
  }
});
observer.observe({ entryTypes: ["mark", "frame"] }); // 监听的性能条目

接下来,我们就进入正题,带大家了解一些比较常用的性能指标

FP 和 FCP

FP(first-paint):首次绘制

  • 指浏览器首次将像素绘制到屏幕上的时间,它标志着浏览器开始绘制页面内容。这个时间点是用户看到页面从空白变为有内容的时刻,哪怕只是一点点内容,比如背景色或者部分简单的布局元素。

FCP(first-contentful-paint):首次有内容的绘制

  • FCP 是指浏览器首次绘制出包含文本、图像、SVG 等任何内容(非空白)的时间。与 FP 相比,FCP 更关注有实际意义的内容出现的时间

怎么理解首次绘制呢?我们通过实践来理解一下

先写一个盒子,然后设置背景色,并将文本延迟1s展示,监听 paint 条目。

<div class="box">
    <div id="myDiv"></div>
</div>

<!-- css -->
.box {
    background: chocolate;
    min-height: 100vh;
}
<!-- js -->
// 设置监听paint条目
const observer = new PerformanceObserver(entryList => {
  for (const entry of entryList.getEntries()) { // 获取对应监听到的条目集合
    console.log(entry);
  }
});
observer.observe({
  entryTypes: ['paint']
});

var div = document.getElementById("myDiv");
setTimeout(() => {
  div.innerText = "这是一个超级超级多的文字*40";
}, 1000);

执行上面的代码 看控制台输出

paint.png

通过log确实看到其监听到了 first-paintfirst-contentful-paint 这两个时刻

我们直接打开谷歌的控制台的 Performance 来记录一下页面的渲染过程,顺便对比一下其与PerformanceObserver记录值的准确性。

fp.gif

可以看到当背景色被渲染出来的时候就是 FP 的时刻即首次绘制;当文本内容渲染出来的时候就是 FCP 时刻即首次有内容绘制

我们接着看上图鼠标移动所悬浮在FPFCP的时间与我们 PerformanceObserver 记录的 startTime是一致的。

如果还是不理解首次绘制的话 我们可以看一些真实的网站记录的两个时刻,如下

paint-timing-filmstrip.avif

LCP

LCP(largest-contentful-paint):指在 可视区域 内,页面上的最大内容元素(如图片、视频或文本块)开始出现在屏幕上的时间点。

老样子,我们在可视区域放置图片和文字,通过变更图片和文字所占的可视区域来对比区分一下页面的最大内容元素,并监听 largest-contentful-paint


observer.observe({
  entryTypes: ['largest-contentful-paint']
});

lcp.png

通过图片可以很清楚的看到最大元素是哪个,以及 PerformanceObserver 所记录的最大元素的DOM和LCP的时间。

FCP 得分多少算好?

为了提供良好的用户体验,网站应尽量将首次有意义的绘制时间控制在 1.8 秒或更短的时间。

fcp.png

优化 LCP

场景:首屏的大图加载慢

  • 可以通过TinyPNG等工具压缩图片
  • 替换 webp/AVIF 格式,减小图片大小
  • 使用 <img>srcset 根据设备分辨率加载合适的图片。

场景:LCP 元素被延迟渲染,因为 CSS 和 JS 文件加载和解析占用了主线程。

  • 可以通过压缩图片 转为webp格式
  • 减少阻塞资源,将非关键 CSS,JS 延迟加载
  • Tree Shaking 和代码分割

FID

FID(‌First Input Delay):用户首次和网站进行交互到浏览器响应该事件的实际延时时间(指从用户点击按钮、链接或与页面进行其他交互的那一刻,到浏览器实际处理并响应该操作的时间。)

很多人会误以为是页面可交互时间,其实是用户与页面首次交互的时间,这两者是有差异的

现在我们给页面加一个按钮并添加点击事件,然后看看是否会记录 first input

<div id="btn">点击</div>

observer.observe({
  entryTypes: ['first-input']
});

var div = document.getElementById("btn");
div.addEventListener('click', () => {
  console.log('click')
})

recording.gif

fid的值近似等于 processingStart - startTime 因为它体现了从用户首次输入(startTime)到浏览器开始处理该输入(processingStart)之间的延迟时间。

CLS

CLS(‌Cumulative Layout Shift):累计位移偏移,记录了页面上非预期的位移波动(用户在页面上滚动或交互时,网页元素意外移动的情况)。

具体是什么意思呢?

cls.png

如图 就是当你在看某篇文章时,有内容没展示出来,等过了会插入了进来,导致原先看到的内容被挤下来,这就是偏移

什么问题会导致CLS?

  • 没有尺寸的图像。
  • 没有尺寸的广告、嵌入和 iframe。
  • 动态注入的内容。
  • Web 字体导致 FOIT/FOUT。
  • CSS 或 JavaScript 动画。

CLS 得分怎样才算理想?

为了提供良好的用户体验,网站应尽力使 CLS 得分不高于 0.1。为确保大多数用户都能达到此目标值,一个合适的衡量阈值是网页加载时间的第 75 个百分位数,并按移动设备和桌面设备进行细分。

cls-value.png

我们来写一个例子来理解一下CLS


<div id="before"></div>
<div id="myDiv">这里是文本内容</div>

// js
observer.observe({
  entryTypes: ['layout-shift']
});
// 1s后插入图片
img.src = 'https:xxxx.png'
setTimeout(() => {
  document.getElementById('before').appendChild(img)
}, 1000);

cls.gif

可以看到1s后 PerformanceObserver 记录了cls条目。

如何计算 CLS

CLS的计算公式 shiftScore = impactFraction * distanceFraction

  • impactFraction(影响分数):该帧和前一帧中所有不稳定元素的可见区域的总和,占视口总区域的百分比,即页面上发生偏移的区域占视口(viewport)面积的比例
  • distanceFraction(距离分数):任何不稳定元素在帧中移动的最长水平或垂直距离除以视口的最大尺寸(宽度或高度,以较大者为准)。

影响分数不好理解,我们就以上图的例子来结合理解一下, 来计算一下其CLS值

cls_1.png

以上图为例(视口宽高为 980*1743)

其中的影响分数的 该帧和前一帧 可以理解为右图和左图,它的CLS的分数计算如下

  • impactFraction(影响分数)= (蓝色框的区域 + 红色区域)的并集
  • distanceFraction(距离分数) = 绿色框区域(即元素移动的距离)
{
    "name": "",
    "entryType": "layout-shift",
    "startTime": 1024.6999998092651,
    "duration": 0,
    "value": 0.14634441423062083,
    "hadRecentInput": false,
    "lastInputTime": 0,
    "sources": [ // 导致布局偏移的元素的位置信息
        {
            "previousRect": {
                "x": 0,
                "y": 0,
                "width": 980,
                "height": 1105,
                "top": 0,
                "right": 980,
                "bottom": 1105,
                "left": 0
            },
            "currentRect": {
                "x": 0,
                "y": 314,
                "width": 980,
                "height": 1105,
                "top": 314,
                "right": 980,
                "bottom": 1419,
                "left": 0
            }
        }
    ]
}

其中PerformanceObserversources 字段描述了导致布局偏移的元素及其变化。每个元素的 currentRectpreviousRect 表示元素的当前和之前的位置。

我们根据PerformanceObserver记录的值代入公式

const viewWidth = 980
const viewHeight = 1743
// 红色区域面基
impactFraction = ((currentRect.bottom - previousRect.top) * viewWidth) / (viewWidth * viewHeight)
distanceFraction = (currentRect.y - previousRect.y) / viewHeight
shiftScore = impactFraction * distanceFraction

这里省略代入值,

  • 经计算得出 shiftScore = 0.14666188728358234
  • PerformanceObserver 计算的 value = 0.14634441423062083

可以发现有一点点偏差,可以忽略不计,因为浏览器在计算 CLS 时会采用 优化算法加权因素,以确保对实际用户体验的准确反映,这些计算不仅仅是基于元素的位移。这里了解一下即可。

如何提高 CLS

  • 对于没有尺寸的图像,请务必为图片和视频元素添加 width 和 height 尺寸属性。或者,使用 CSS aspect-ratio或类似方法预留所需空间。这种方法可确保浏览器在图片加载时能够在文档中分配正确数量的空间。
  • 在动画中请避免使用 top left 这些属性设置动画,因为会导致布局偏移
  • 对于一些字体的影响可以通过设置 font-display: optional 减少字体加载影响

TTFB

TTFB(‌Time to First Byte):是一个指标,用于衡量请求资源到响应第一个字节开始到达之间的时间。

网络请求阶段及其相关时间的示意图。TTFB 用于衡量 startTime 到 responseStart 之间的经过时间。

这是网络请求阶段及其相关时间的示意图。TTFB 用于衡量 startTime 到 responseStart 之间的经过时间。

TTFB 是以下请求阶段的总和:

  • 重定向时间
  • Service Worker 启动时间(如果适用)
  • DNS 查找
  • 连接和 TLS 协商
  • 请求,直到响应的第一个字节到达

TTFB 得分多少算好?

大多数网站应力求 TTFB 不超过 0.8 秒

ttfb_value.png

observer.observe({
  entryTypes: ['navigation']
});

const ttfb = entry.responseStart - entry.requestStart
  • entry.responseStart:响应开始的时间(即服务器开始返回响应的时间)。

  • entry.requestStart:请求开始的时间(即浏览器发出请求的时间)。

由于 TTFB 不是核心网页指标,因此网站不一定必须达到“良好”TTFB 阈值,前提是 TTFB 不会妨碍网站在重要指标方面获得理想得分。

核心网页指标

其中核心网页指标侧重于用户体验的三个方面:加载速度互动性视觉稳定性。其中包括以下指标

main.png

加载速度、互动性和视觉稳定性这三个方面相互关联、缺一不可,共同构成了优质的用户体验。所以我们应高度重视核心网页指标。

总结

在线上项目中,有很多问题可能是我们自己本地测试或者遇不到的场景导致的,比如一些手机的兼容性,一些异常的网络信号等等场景,这些场景都有可能会影响到项目的运行,所以为了提升用户的体验和项目的性能,就需要我们通过一些能力去全面的监测,其中 PerformanceObserver 可以很好的帮助我们针对不同的指标进行一个监测,只要我们合理的运用,就可以让用户的体验变得更加丝滑。