聊聊 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 标准中包括了如下三点:

  1. 扩展了 Performance 接口的基本定义
  2. Web Workers 中暴露了 PerformanceEntry
  3. 增加了 PerformanceObserver 的支持

此时我们本篇文章的主角:PerformanceObserver 带着它自身的使命终于出现了。

正文

了解了 PerformanceObserver 出现的背景,接下来我们就重点了解下它的具体用法吧。

1.1 简介

PerformanceObserver 主要用于监测性能度量事件,在浏览器的性能时间轴记录新的 performanceEntry(详见下方介绍) 时会被通知。

通过使用 PerformanceObserver() 构造函数我们可以创建并返回一个新的 PerformanceObserver 对象,从而进行性能的监测。

也就是说,性能指标可以通过 window.performance 获取,但是获取什么时候的指标就可以通过 PerformanceObserver 构造函数生成的实例,在不同的时机拿到对应不同的值。

1.2 优点

简单介绍了 PerformanceObserver 出现的背景和它是什么,那它的优势具体有哪些呢,它其实主要解决了以下 3 个问题:

  1. 避免不知道性能事件啥时候会发生,需要重复轮询 timeline 获取记录。
  2. 避免产生重复的逻辑去获取不一样的性能数据指标。
  3. 避免其余资源需要操作浏览器性能缓冲区时产生竞态关系。

W3C 官网文档鼓励开发人员尽量使用 PerformanceObserver,而不是经过 Performance 获取性能参数及指标。另外,新的性能 API 和指标可能只能经过 PerformanceObserver 接口得到。

1.3 PerformanceEntry

由于 PerformanceObserver 的使用离不开 PerformanceEntry,所以在具体了解 PerformanceObserver 之前,我们有必要先了解下 PerformanceEntry 是什么。

1.3.1 mdn 中的介绍:

PerformanceEntry

意思大概是,在页面运行过程中我们可以通过 PerformanceEntry 实例持续获取性能数据,拿到粒度更细的 performance 数据信息,也称为 performance metric。而 PerformanceEntry 可以通过 performance.getEntries() 获取到,用项目中其中一个页面打印一下,大概如下:

PerformanceEntry

可以看出,打印出来的是一个数组,而数组中的每一项都是 PerformanceEntry 的实例。
PerformanceEntry 可以理解为描述浏览器中某一个行为的性能指标,比如获取资源文件的性能、一次事件的性能、渲染页面的性能等等。

1.3.2 PerformanceEntry 实例

说起 PerformanceEntry 数组中的实例,主要包括这么几个:

PerformanceResourceTiming:该实例主要用于确定页面各个资源加载所花费的时间,比如获取 css、js、img 文件、http 接口等资源请求花费的时间。通过看 entryType 字段可以看到对应的 entryTyperesource

PerformanceResourceTiming

PerformanceNavigationTiming:这个实例用于确定一个页面从开始加载到结束需要的时间。通过看 entryType 字段可以看到对应的 entryTypenavigation

PerformanceNavigationTiming

PerformancePaintTiming:这个实例主要是获取页面刷新渲染过程中,第一个像素点呈现在页面上的时间(first paint),以及第一个 dom 元素呈现在页面上的时间(first contentful paint)。通过看 entryType 字段可以看到对应的 entryTypepaint


PerformancePaintTiming

下表是以上字段的说明解释:

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,其中就包含了上方我们列出的部分实例。

entryType

1.4 方法

了解了 PerformanceEntry,接下来我们了解下 PerformanceObserver 具体有哪些方法。

PerformanceObserver 主要有3个方法。

a. PerformanceObserver.observe():当传参中的性能指标在上方指定的 entryTypes 之中时,该性能指标的回调函数将会被调用并返回。
b. PerformanceObserver.disconnect():停止性能观察者回调接收到性能指标。
c. PerformanceObserver.takeRecords():返回存储在性能观察器中的性能指标的列表,并将其清空。

只有定义没有例子的文章不是一篇合格的技术文,接下来我们就通过示例来了解下 PerformanceObserver 方法的具体用法。

1.5 示例

我们重点用 PerformanceObserverobserve() 这个方法,分别简单实现下性能优化方面比较常见的几个性能指标数据吧。

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 的内容:

获取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 的内容:

)获取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 的内容:

获取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 的值,如下:

获取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 的值:

获取FID

通过监测用户访问页面的这些性能数据,可以直观的看到在哪些方面我们需要进行性能提升,从而可以根据不同页面的展现情况针对性的提高用户体验。

1.6 兼容性

了解了 PerformanceObserver 的具体做法,紧接着我们看下它目前的兼容性吧。

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,
})

具体思路大致如下

  1. 利用 MutationObserver 监听 document 对象,每当 DOM 元素属性发生变更时,触发事件。
  2. 判断该 DOM 元素是否在首屏内,如果在,则在 requestAnimationFrame() 回调函数中调用 performance.now() 获取当前时间,作为它的绘制时间。
  3. 将最后一个 DOM 元素的绘制时间和首屏中所有加载的图片时间作对比,将最大值作为首屏渲染时间。
    这样就通过 MutationObserver 简单获取了首屏时间。

总结

至此,我们的 PerformanceObserver 就基本介绍完成了。PerformanceObserver 这个 API 涉及的内容挺多,本篇文章就针对以下几个内容简单介绍了下:

  1. 平时我们页面的性能指标数据可以通过 window.performance 获取,但是获取的时机就可以通过 PerformanceObserver 进行监测获取到对应指标。
  2. 我们可以通过 PerformanceEntry 实例持续获取性能数据,拿到粒度更细的流程的 performance 数据信息。
  3. 使用 PerformanceObserver 提供的方法 observe() 方法简单实现了 FPFCPLCPCLSFID 指标数据的获取。
  4. 最后我们简单实现了通过 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

posted @ 2023-04-06 14:18  沐子馨  阅读(673)  评论(0编辑  收藏  举报