# performance 与 performanceObserver的对比与实践

关于性能指标的计算,目前有 performanceTiming、performanceNavigationTiming、performanceObserver 三种方式,其中 performanceTimg 已经废弃,被 performanceNavigationTiming 取代。目前主流性能计算是用的 performanceObserver 的方式,之前在某公司实习时,公司内部的性能监控平台的数据采集,就是使用 performanceObserver 来做的。 下面我们来简单讲解一下这三种性能相关的 API。

# performanceTiming

MDN:PerformanceTiming (opens new window)

performanceTiming 接口提供了在加载和使用当前页面期间发生的各种事件的性能计时信息。需要注意的是,该 API 已被弃用,不过考虑到向下兼容的问题,目前还暂时被保留。被弃用的主要原因是因为接口提供的时间精度不够,随着前端的发展,目前前端性能相关的时间计算已经达到纳秒的级别,而该 API 只能提供毫秒级的时间。

下面我们利用 performanceTiming 来简单计算一下网站域名解析的时间:

        window.onload = function () {
            const start = window.performance.timing.domainLookupStart
            const end = window.performance.timing.domainLookupEnd
            console.log('domain', end - start);
        }

由于我是直接加载的本地html,无需进行域名解析,所以打印结果为 0。

下面我们再利用 performanceTiming 来计算一下网站 DOM 的解析处理时间。

        window.onload = function () {
            const domComplete = window.performance.timing.domComplete
            const domLoading = window.performance.timing.domLoading
            console.log('DOM', domComplete - domLoading);
        }

performanceTiming 需要我们手动去计算每种性能指标,并且最大的问题在于精度不够,所以已被废弃。

# performanceNavigationTiming

MDN:PerformanceNavigationTiming (opens new window)

PerformanceNavigationTiming 提供了用于存储和检索有关浏览器文档事件的指标的方法和属性。例如,此接口可用于确定加载或卸载文档需要多少时间。

下面我们来看一下 performanceNavigationTiming 对象中的属性:

可以看到,其字段名称和 performanceTiming 几乎一致,但时间精度精确到了纳秒级。

但是,performanceTiming 也有局限性,那就是我们只能在当前这个时间点进行采集,采集之后可能会发生一些事件的性能是我们监听不到的。比如我们一般会在 onload 的回调中采集这些指标,但是在采集之后,可能会有额外的 DOM 渲染,或者是还有一些动画在加载显示。这些在 onload 事件后发生的事件的性能,无法被监听到。简而言之,就是我们不知道性能事件何时会发生时,就需要重复轮询获取 performanceNavigationTiming 记录

这时我们采用 performanceObserver 就会十分方便。

# performanceObserver

性能监测对象 (opens new window)

PerformanceObserver 用于监测性能度量事件,在浏览器的性能时间轴记录新的 performance entry (opens new window) 的时候将会被通知。performanceObserver 接收一个回调,当监听的性能指标变化时,会执行该回调。

        // 回调
        function cb(list, observe) {
            console.log('回调执行了');
            list.getEntries().forEach(e => {
                console.log(e);
            })
        }

        // 创建 PerformanceObserver 对象
        let observer = new PerformanceObserver(cb)
        //  指定要监听的性能数据类型
        observer.observe({ entryTypes: ['paint', 'mark'] })

        // 打点
        window.performance.mark('own')

输出结果:

当有有性能指标变化时,回调就会被执行。我们可以看到,通过监听 **paint**数据,可以很容易的直接获取到 FP、FCP 的值,也就是白屏时间和首屏时间。包括我们使用 performance.mark 打点的时间也可以被 performanceObserver 监听到,可以获取到每个点的时间,通过打点,我们可以各种计算我们想要的性能数据。

# FP、FCP 的采集

如果可以确保页面已经完成了首次绘制和首次内容绘制,可以使用 performance.getEntries 直接获取。

window.performance.getEntriesByType('paint')

window.performance.getEntriesByName('first-paint')
window.performance.getEntriesByName('first-contentful-paint')

如果页面还没有开始首次绘制,就需要通过 performanceObserver 监听获取。

        // 回调
        function cb(list, observe) {
            list.getEntries().forEach(e => {
                console.log(e);
            })
        }

        // 创建 PerformanceObserver 对象
        let observer = new PerformanceObserver(cb)
        //  指定要监听的性能数据类型
        observer.observe({ entryTypes: ['paint'] })

# LCP 的采集

LCP 主要依赖 PerformanceObserver,具体监听方式如下:

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 事件会在页面加载过程中根据此时已渲染最大元素的变化,不断的被触发,实际上报中会一直监听这些变化,直到用户与页面发生交互行为(比如 click、keydown)或者页面被隐藏或者页面被 unload 等,取监听到的最后值做上报。

浏览器会多次报告 LCP ,而一般真正的 LCP 是用户交互前最近一次报告的 LCP ,因为交互往往会改变用户可见的内容,所以用户交互后新报告的 LCP 不再符合 LCP 的指标定义,故交互之后的 LCP 我们不再采纳。

# FMP 的采集

FMP 的标准比较模糊,对于 "有意义的内容" 的标准并不统一,所以没有统一的算法。比较常见的算法是利用 “DOM 变化最剧烈的时间点” 来表示 FMP,也就是我们在 《首屏时间指标及采集方法》中介绍的使用 mutationObserver 计算首屏时间的方式。