聊聊 PerformanceObserver
公众号链接:https://mp.weixin.qq.com/s/o0ucrZUfMn2Tu3OydkDx3g
背景
在用户体验越发重要的今天,关注页面性能、提升页面展现速度及交互体验对前端开发越来越重要。
为了监测页面性能,chrome 开发团队就提出过监测网页性能的一些指标,比如 FP、FCP 等,还有我们公司自己的北斗网站的秒开率、快开比等。但是这些指标具体怎样获取呢,今天我们就来仔细了解下这个性能监控有关的 API:PerformanceObserver
。
历史
说起 PerformanceObserver
,我们首先要先了解下 Performance Timeline
。
Performance Timeline
是 W3C 性能小组提出的一个规范,定义了让开发者在应用整个生命周期内收集各类性能指标的接口。
一般情况下,我们想得到某项性能记录,需要知道指定的性能事件已经发生,比如使用定时轮询的方式,主动调用 performance.getEntries 或者 performance.getEntriesByName 来获取。
为了解决这个问题,在 Performance Timeline Level 2
中,除了扩展了 Performance
的基本定义之外,还增加了 PerformanceObserver
接口,于是最新的 Performance Timeline Level 2
标准中包括了如下三点:
- 扩展了
Performance
接口的基本定义 - 在
Web Workers
中暴露了PerformanceEntry
- 增加了
PerformanceObserver
的支持
此时我们本篇文章的主角:PerformanceObserver
带着它自身的使命终于出现了。
正文
了解了 PerformanceObserver
出现的背景,接下来我们就重点了解下它的具体用法吧。
1.1 简介
PerformanceObserver
主要用于监测性能度量事件,在浏览器的性能时间轴记录新的 performanceEntry
(详见下方介绍) 时会被通知。
通过使用 PerformanceObserver() 构造函数我们可以创建并返回一个新的 PerformanceObserver
对象,从而进行性能的监测。
也就是说,性能指标可以通过 window.performance
获取,但是获取什么时候的指标就可以通过 PerformanceObserver
构造函数生成的实例,在不同的时机拿到对应不同的值。
1.2 优点
简单介绍了 PerformanceObserver
出现的背景和它是什么,那它的优势具体有哪些呢,它其实主要解决了以下 3 个问题:
- 避免不知道性能事件啥时候会发生,需要重复轮询
timeline
获取记录。 - 避免产生重复的逻辑去获取不一样的性能数据指标。
- 避免其余资源需要操作浏览器性能缓冲区时产生竞态关系。
W3C 官网文档鼓励开发人员尽量使用 PerformanceObserver
,而不是经过 Performance
获取性能参数及指标。另外,新的性能 API 和指标可能只能经过 PerformanceObserver
接口得到。
1.3 PerformanceEntry
由于 PerformanceObserver
的使用离不开 PerformanceEntry
,所以在具体了解 PerformanceObserver
之前,我们有必要先了解下 PerformanceEntry
是什么。
1.3.1 mdn 中的介绍:
意思大概是,在页面运行过程中我们可以通过 PerformanceEntry
实例持续获取性能数据,拿到粒度更细的 performance
数据信息,也称为 performance metric
。而 PerformanceEntry
可以通过 performance.getEntries() 获取到,用项目中其中一个页面打印一下,大概如下:
可以看出,打印出来的是一个数组,而数组中的每一项都是 PerformanceEntry
的实例。
PerformanceEntry
可以理解为描述浏览器中某一个行为的性能指标,比如获取资源文件的性能、一次事件的性能、渲染页面的性能等等。
1.3.2 PerformanceEntry 实例
说起 PerformanceEntry
数组中的实例,主要包括这么几个:
PerformanceResourceTiming:该实例主要用于确定页面各个资源加载所花费的时间,比如获取 css、js、img 文件、http 接口等资源请求花费的时间。通过看 entryType
字段可以看到对应的 entryType
是 resource
。
PerformanceNavigationTiming:这个实例用于确定一个页面从开始加载到结束需要的时间。通过看 entryType
字段可以看到对应的 entryType
是 navigation
。
PerformancePaintTiming:这个实例主要是获取页面刷新渲染过程中,第一个像素点呈现在页面上的时间(first paint),以及第一个 dom
元素呈现在页面上的时间(first contentful paint)。通过看 entryType
字段可以看到对应的 entryType
是 paint
。
下表是以上字段的说明解释:
key | 说明 |
---|---|
connectEnd | HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间 |
connectStart | HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 |
decodedBodySize | 从HTTP或缓存中获取的消息体积大小 |
domainLookupEnd | DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 |
domainLookupStart | DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 |
duration | 加载时间 |
encodedBodySize | 从HTTP或缓存中获取的body体积大小 |
entryType | 资源类型,entryType类型不同数组中的对象结构也不同 |
fetchStart | 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前 |
initiatorType | 谁发起的请求 |
name | 资源名称,是资源的绝对路径或调用mark方法自定义的名称 |
nextHopProtocol | 获取资源使用的网络协议 |
redirectEnd | 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内的重定向才算,否则值为 0 |
redirectStart | 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 |
requestStart | HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存,连接错误重连时,这里显示的也是新建立连接的时间 |
responseEnd | HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存 |
responseStart | HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存 |
secureConnectionStart | HTTPS 连接开始的时间,如果不是安全连接,则值为 0 |
serverTiming | 包含服务时间元数据的数组 |
startTime | 开始时间 |
transferSize | 加载资源的体积大小,包含请求头及请求体 |
workerStart | DOMHighResTimeStamp |
1.3.3 entryType
通过下方表格可以看出,entryType
主要有 6 种类型,分别对应 6 个 subtype
,其中就包含了上方我们列出的部分实例。
1.4 方法
了解了 PerformanceEntry
,接下来我们了解下 PerformanceObserver
具体有哪些方法。
PerformanceObserver
主要有3个方法。
a. PerformanceObserver.observe():当传参中的性能指标在上方指定的 entryTypes 之中时,该性能指标的回调函数将会被调用并返回。
b. PerformanceObserver.disconnect():停止性能观察者回调接收到性能指标。
c. PerformanceObserver.takeRecords():返回存储在性能观察器中的性能指标的列表,并将其清空。
只有定义没有例子的文章不是一篇合格的技术文,接下来我们就通过示例来了解下 PerformanceObserver
方法的具体用法。
1.5 示例
我们重点用 PerformanceObserver
的 observe()
这个方法,分别简单实现下性能优化方面比较常见的几个性能指标数据吧。
a. FP(first-paint): 从页面加载开始到第一个像素绘制到屏幕上的时间,也可以把 FP
理解成白屏时间。
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-paint') {
observer.disconnect()
}
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })
通过以上代码可以得到 FP
的内容:
其中 startTime
就是我们要的绘制时间。
b. FCP(first-contentful-paint): 从页面加载开始到所有页面内容在屏幕上完成渲染的时间。
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
observer.disconnect()
}
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })
通过以上代码可以得到 FCP
的内容:
其中 startTime
就是我们要的绘制时间。
c. LCP(largest-contentful-paint): 从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP
指标会根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间。
const entryHandler = (list) => {
if (observer) {
observer.disconnect()
}
for (const entry of list.getEntries()) {
console.log(entry)
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'largest-contentful-paint', buffered: true })
通过以上代码可以得到 LCP
的内容:
其中 startTime
就是我们要的绘制时间。element
是指 LCP
绘制的 DOM
元素。
d. CLS(layout-shift): 从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数。
布局偏移分数的计算方式如下:
布局偏移分数 = 影响分数 * 距离分数
let cls = 0;
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
console.log('Current CLS value:', cls, entry);
}
}
};
const observer = new PerformanceObserver(entryHandler);
observer.observe({type: 'layout-shift', buffered: true});
通过以上代码得到 CLS
的值,如下:
e. FID(First Input Delay):指用户交互事件触发到页面响应中间耗时多少。
const entryHandler = (list) => {
for (const entry of list.getEntries()) {
const delay = entry.processingStart - entry.startTime;
console.log('FID :', delay, entry);
}
};
const observer = new PerformanceObserver(entryHandler);
observer.observe({type: 'first-input', buffered: true});
通过运行上方代码可以得到当前页面 FID
的值:
通过监测用户访问页面的这些性能数据,可以直观的看到在哪些方面我们需要进行性能提升,从而可以根据不同页面的展现情况针对性的提高用户体验。
1.6 兼容性
了解了 PerformanceObserver
的具体做法,紧接着我们看下它目前的兼容性吧。
从表中我们可以看出,PerformanceObserver
的兼容性其实已经覆盖大部分浏览器了。对于比如 IE 或者移动端部分不兼容的浏览器,可以考虑使用我们下方介绍的观察者 Observer
中的 MutationObserver
进行代替。
而 MutationObserver
又是什么呢,接下来我们再简单介绍下。
1.7 MutationObserver
MutationObserver
在监听的 DOM
元素属性发生变化时会触发事件,与 PerformanceObserver
相同,也是一个观察者 API。
接下来我们通过 MutationObserver
简单实现下获取 FCP
首屏渲染时间。
const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout;
const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK']
observer = new MutationObserver(mutationList => {
const entry = {
children: [],
}
for (const mutation of mutationList) {
if (mutation.addedNodes.length && isInScreen(mutation.target)) {
// ...
}
}
if (entry.children.length) {
entries.push(entry);
next(() => {
entry.startTime = performance.now()
})
}
})
observer.observe(document, {
childList: true,
subtree: true,
})
具体思路大致如下
- 利用
MutationObserver
监听document
对象,每当 DOM 元素属性发生变更时,触发事件。 - 判断该
DOM
元素是否在首屏内,如果在,则在requestAnimationFrame()
回调函数中调用performance.now()
获取当前时间,作为它的绘制时间。 - 将最后一个
DOM
元素的绘制时间和首屏中所有加载的图片时间作对比,将最大值作为首屏渲染时间。
这样就通过MutationObserver
简单获取了首屏时间。
总结
至此,我们的 PerformanceObserver
就基本介绍完成了。PerformanceObserver
这个 API
涉及的内容挺多,本篇文章就针对以下几个内容简单介绍了下:
- 平时我们页面的性能指标数据可以通过
window.performance
获取,但是获取的时机就可以通过PerformanceObserver
进行监测获取到对应指标。 - 我们可以通过
PerformanceEntry
实例持续获取性能数据,拿到粒度更细的流程的 performance 数据信息。 - 使用
PerformanceObserver
提供的方法observe()
方法简单实现了FP
、FCP
、LCP
、CLS
、FID
指标数据的获取。 - 最后我们简单实现了通过
MutationObserver
获取 FCP 数据。
作者简介:
李馨馨:日常热衷中医养生的佛系 girl~
参考文献:
[1] 现代浏览器观察者 Observer API 指南: https://cloud.tencent.com/developer/article/1528620
[2] https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_Timeline
[3] https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver
[4] https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry/entryType
[5] https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceNavigationTiming
[6] https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
[7] https://developer.chrome.com/blog/performance-observer/
[8] https://www.qmblog.cn/14819.html