深入使用 PerformanceObserver

2,944 阅读8分钟

前言

在前端开发中,性能优化总是绕不开的话题。我们都知道,一个交互流畅的网页能够大大提升用户体验,留住更多用户。而要实现这一目标,精准地监控和分析网页性能就显得尤为重要。提到性能监控,你可能会想到performance API,但今天,我想带你一起探索一个更加灵活、强大的工具——PerformanceObserver。

PerformanceObserver并不是要完全取代performance API,而是提供了一个更加主动、事件驱动的方式来监听和处理性能数据。想象一下,你不再需要手动去查询或轮询性能数据,而是可以设置一个观察者,让它在你关心的性能事件发生时自动通知你。

我们可以通过 PerformanceObserver 监测如下数据,也许你已经使用过这些性能指标,并且想深入了解下如何获取这些指标可以跟着文章一探究竟! yuque_diagram.jpg

为什么使用 PerformanceObserver

那么,为什么我们要选择PerformanceObserver呢?它的独特优势在于:

  1. 实时性:PerformanceObserver能够实时捕获性能事件,让你在第一时间了解网页的性能表现。这对于及时发现并解决问题至关重要。
  2. 灵活性:通过配置PerformanceObserver的回调函数,你可以自定义处理性能数据的方式。无论是简单的日志记录,还是复杂的性能分析,都能轻松应对。
  3. 可扩展性:随着Web标准的不断发展,PerformanceObserver支持的性能条目也在不断增加。这意味着你可以用它来监控更多类型的性能数据,满足日益增长的性能优化需求。
  4. 易用性:虽然PerformanceObserver提供了强大的功能,但它的API设计相对简洁直观。即使是初学者,也能较快上手并应用到实际项目中。

PerformanceObserver

基础示例

下面我将通过一个简单的例子来介绍PerformanceObserver的基础使用。这个例子将展示如何设置一个PerformanceObserver来监听页面上的resource性能条目,即资源加载事件。

// 创建一个 PerformanceObserver 实例  
const performanceObserver = new PerformanceObserver((list) => {  
  // 回调函数会在每次有匹配的 PerformanceEntry 被添加到 PerformanceTimeline 时被调用  
  for (const entry of list.getEntries()) {  
    // 检查 entry 类型是否为我们关注的 'resource'  
    if (entry.entryType === 'resource') {  
      console.log(`Resource loaded: ${entry.name}`);  
      console.log(`Duration: ${entry.duration} ms`);  
      console.log(`Initiator Type: ${entry.initiatorType}`); // 哪个类型的事件触发了这个资源的加载  
      // 可以根据需要添加更多日志或处理逻辑  
    }  
  }  
});  
  
// 告诉 PerformanceObserver 我们想要监听哪些类型的 PerformanceEntry  
// 在这个例子中,我们监听 'resource' 类型的条目  
performanceObserver.observe({ type: 'resource' });  

静态方法

通过上面的基础示例了解了 PerformanceObserver 的使用, 除了监听 resource 还可以监听其他的类型。你可以通过PerformanceObserver 的静态方法 supportedEntryTypes 查询当前浏览器支持哪些类型的性能条目(PerformanceEntry)。

// 检查浏览器支持的 PerformanceEntry 类型  
const supportedTypes = PerformanceObserver.supportedEntryTypes; 

image.png

实例方法

  • 创建实例
  • 通过实例 observe() 监听多个性能条目
  • 可以通过 disconnect() 取消监听性能条目
// 创建实例,当记录指定类型的性能条目出现时,性能监测对象的回调函数将会被调用。
const observer = new PerformanceObserver(function (list, obj) {
  var entries = list.getEntries();
  for (var i = 0; i < entries.length; i++) {
    // mark、element 的性能条目会在这里触发
  }
});
// 监听指定性能条目,
observer.observe({ type: 'mark' });
// 可设置多个监听
observer.observe({ type: 'element' });

setTimeout(()=> {
    // 取消监听
    observer.disconnect();
}, 3000)

observer.observe()

针对 observe 方法需要详细再说明下。他支持多个参数

observer.observe({
    type: 'navigation',
    buffer: true
})
  • type
    type 是一个字符串,用于指定您只关心的一种性能条目类型。例如,如果您只关心页面加载(navigation)的性能数据,就可以使用此选项。

  • bufferd
    是否缓存加载过的性能条目,这样在 observe 监控调用之前发生的性能条目也会触发回调。必须与 type 选项一起使用。

  • entryTypes 一个字符串对象的数组,每个字符串指定一个要观察的性能条目类型。不能与 “type”、“buffered” 或 “durationThreshold” 选项一起使用。

  • durationThreshold
    当你使用 PerformanceObserver API 来观察浏览器的性能条目(performance entries)时,durationThreshold 是一个可选的配置项,它允许你设置一个阈值,以便只接收那些持续时间超过该阈值的条目。

    1. durationThreshold 的默认值是 104ms:这意味着,默认情况下,PerformanceObserver 只会向你报告那些持续时间超过 104 毫秒的性能条目。
    2. 设置为 16ms 以获取更多交互:由于许多用户交互(如点击、滚动等)的响应时间通常远小于 104 毫秒,因此如果你对这类快速交互感兴趣,你可能需要将 durationThreshold 降低到 16 毫秒或更低。这样做可以让你捕获到更多的交互事件,并了解它们的性能特性。
    3. 最小 durationThreshold 是 16ms:这是 API 的一个限制,你不能将 durationThreshold 设置为小于 16 毫秒的值。这是因为低于这个值的性能条目可能对于大多数应用来说并不重要,而且过于频繁地触发观察者可能会导致性能下降。

    请注意,durationThreshold 主要与那些具有持续时间的性能条目相关,如 longtask、event 类型的条目。对于其他类型的条目(如 mark、measure 等),这个阈值可能不适用或具有不同的含义。

PerformanceEntry

基本介绍

PerformanceEntry是一个通用接口,它定义了一系列属性,如name(性能条目的名称)、entryType(性能条目的类型)、startTime(开始时间戳)、duration(持续时间,如果适用)等,用于描述一个性能事件的各个方面。不同的性能事件(如资源加载、页面渲染等)会生成不同类型的PerformanceEntry对象,这些对象都是PerformanceEntry的子类,各自拥有一些特定的属性和方法。

PerformanceObserver会自动为你捕获PerformanceEntry,并在你指定的回调函数中将它们作为参数传递给你。

每当有匹配的性能事件发生时,PerformanceObserver就会调用你的回调函数,并将一个包含新性能条目的PerformanceObserverEntryList对象作为参数传递给你。你可以通过遍历 PerformanceObserverEntryList.getEntries() 来访问每个PerformanceEntry对象。

const p = new PerformanceObserver(list => {
    // PerformanceObserverEntryList 对象
    const entries = list.getEntries()
    for(let entry of entries) {
        console.log('entry:', entry)
    }
})
p.observe({
    type: 'navigation',
    buffered: true
})

一旦你获得了PerformanceEntry对象,你就可以利用它提供的属性和方法来分析和优化你的网页性能了。

image.png

FP、FCP / PerformancePaintTiming

接下来我们通过监控具体的性能条目,实现具体的性能指标的记录和计算。有些性能指标可以直接读取例如: first-paintfirst-contentful-paint

const p = new PerformanceObserver(e => {
    e.getEntries().forEach(entry => {
        console.log(entry)
    })
})
p.observe({
    type: 'paint',
    buffered: true
})

image.png

INP(下一次渲染时间) / PerformanceEventTiming

有些性能指标需要逻辑计算获取例如:INP

    function observeINP() {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries()
        let inpTime = 0
        // 将所有的事件耗时进行统计,取最大耗时
        for (let entry of entries) {
          const inputDelay = entry.processingStart - entry.startTime
          const processingTime = entry.processingEnd - entry.processingStart
          const duration = entry.duration
          const eventType = entry.name
          console.log('----- INTERACTION -----', eventType)
          inpTime = Math.max(inpTime, duration)
        }
        // INP 时间
        console.log('--------total-------' + inpTime)
      })
      observer.observe({
        type: 'event',
        buffer: true,
        // durationThreshold: 50 // 可设置监听性能条目的阀值,超过50ms 就触发回调 默认是 104ms
      })

image.png

TTI (Time To Interactive) / PerformanceLongTaskTiming

TTI TTI 指标用于衡量从网页开始加载到其主要子资源加载完成所用的时间,并且能够快速可靠地响应用户输入。 如需根据网页的性能跟踪记录计算 TTI,请按以下步骤操作:

  1. 从 First Contentful Paint (FCP) 开始。
  2. 向前搜索一个至少 5 秒的静默窗口,其中静默窗口的定义为:没有长任务,且不超过两个进行中的网络 GET 请求。
  3. 向后搜索静默窗口之前的最后一个长任务,如果找不到长任务,则停止在 FCP 处停止。
  4. TTI 是安静窗口之前的最后一个长任务的结束时间(如果未找到长任务,则与 FCP 值相同)。 下图应有助于直观呈现上述步骤:

image.png

计算 TTI

  function calcTTI() {
      console.log('calc calcTTI')
      let lcpStartTime = 0 // 记录 LCP 时间
      const idleTime = 5000 // 定义空闲时间
      const longTaskEntryList = [] // 记录长任务
      let getRequestList = [] // 记录 get 请求
      let timer
      return new Promise((resolve, reject)=> {
        const observer = new PerformanceObserver(list => {
            try {
                
                const entries = list.getEntries()
                const callback = () => {
                  const afterLcpLongTaskList = longTaskEntryList.filter(entry => entry.startTime >= lcpStartTime)
                  // 是否有长任务
                  const lastEntry = afterLcpLongTaskList[afterLcpLongTaskList.length - 1]
                  // 取最后一个长任务,看是否满足 TTI 条件
                  if (lastEntry) {
                    const { startTime, duration } = lastEntry
                    // 阻塞总时间
                    const TBT = startTime - lcpStartTime + duration
                    resolve({TBT, TTI: startTime + duration})
                  } else { 
                    // LCP 之后没有长任务
                    resolve(0)
                  }
                  timer && clearTimeout(timer)
                }
                for (entry of entries) {
                  console.log('---calc-----', entry.entryType, entry)
                  // 确定 LCP
                  if (entry.entryType === 'largest-contentful-paint') {
                    lcpStartTime = entry.startTime
                    // LCP 可能触发多次
                    if (timer) {
                      clearTimeout(timer)
                    }
                    // 设定定时器,用于在 5s 空闲时间后执行
                    // 判断是否为 TTI 条件之一
                    timer = setTimeout(callback, idleTime)
                  }
      
                  switch (entry.entryType) {
                      case 'longtask' : {
                          console.log('longtask')
                          longTaskEntryList.push(entry)
                          break
                      }
                      case 'resource' : {
                          const isRequest = entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch'
                          // 只计算 lcp 之后的请求
                          if (isRequest && getURLList.includes(entry.name) && lcpStartTime) {  
                              getRequestList.push(entry)
                          }  
                          // 如果累计两个 get 请求,重新设置定时器继续等待
                          if (getRequestList.length >= 2) {
                              console.log('存在两个 get 请求',getRequestList)
                              clearTimeout(timer)
                              timer = setTimeout(callback, idleTime)
                              getRequestList = []
                          }
                      }
                  }
                }
            } catch (error) {
                reject(error)
            }
        })
        observer.observe({
          type: 'longtask',
          buffered: true
        })
       // 用于计算LCP,TTI 中需要使用
        observer.observe({
          type: 'largest-contentful-paint',
          buffered: true
        })
        // 用于查看是否存在 GET 请求
        observer.observe({
            type: 'resource',
            buffered: true
        })
      })
    }

image.png

其他

完整代码以及其他性能指标可以查看我仓库代码 image.png

结语

PerformanceObserver以其非侵入式的监控方式、实时准确的数据捕获、高度自定义的灵活性以及强大的数据处理能力,为我们提供了有效的性能监控体验。结合实际性能指标的计算如 TTI、INP、FP 等可以更好的理解这些指标背后的原理。

仓库代码