自定义性能指标(翻译)

1,473 阅读12分钟

原文:web.dev/custom-metr…

拥有以用户为中心的度量标准是非常有价值的,你可以在任何给定的网站上普遍地进行度量。这些指标允许您:

  • 了解真实用户对整个网络的体验

  • 很容易将您的网站与竞争对手的网站进行比较

  • 在分析工具中跟踪有用且可操作的数据,而无需编写自定义代码

通用度量提供了一个很好的基线,但是在许多情况下,为了获取特定站点的完整体验,您需要度量的不仅仅是这些度量。

自定义指标允许您衡量站点体验的各个方面,这些方面可能只适用于您的站点,例如:

  • 单页应用程序(SPA)从一个“页面”转换到另一个“页面”需要多长时间

  • 页面为登录用户显示从数据库获取的数据所需的时间

  • 服务器端呈现(SSR)应用程序整合需要多长时间

  • 返回访问者加载的资源的缓存命中率

  • 游戏中点击或键盘事件的事件延迟

用于度量自定义度量的API#

从历史上看,web开发人员并没有多少底层api来衡量性能,因此他们不得不求助于黑客攻击来衡量一个站点是否表现良好。

例如,可以通过运行requestAnimationFrame循环并计算每个帧之间的增量来确定是否由于长时间运行的JavaScript任务而阻塞了主线程。如果增量明显长于显示器的帧速率,则可以将其报告为长任务。不过,不建议使用这种黑客攻击,因为它们实际上会影响性能(例如,通过耗尽电池)。

有效的性能度量的第一条规则是确保性能度量技术本身不会导致性能问题。因此,对于在站点上度量的任何自定义度量,如果可能,最好使用以下api之一。

Performance Observer

理解PerformanceObserver API对于创建自定义性能指标至关重要,因为它是从本文讨论的所有其他性能API获取数据的机制。

使用PerformanceObserver,您可以被动地订阅与性能相关的事件,这意味着这些api通常不会干扰页面的性能,因为它们的回调通常是在空闲期间激发的。

通过向PerformanceObserver传递一个回调函数来创建PerformanceObserver,该回调函数将在调度新的性能项时运行。然后通过observe()方法告诉观察者要监听哪些类型的条目:

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 记录条目和所有相关的详细信息。
      console.log(entry.toJSON());
    }
  });

  po.observe({type: 'some-entry-type'});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

下面的部分列出了所有可供观察的条目类型,但是在较新的浏览器中,您可以通过static检查哪些条目类型是可用的PerformanceObserver.supportedEntryTypes属性。

传递给observe()方法的对象还可以指定entryTypes数组(以便通过同一观察者观察多个条目类型)。虽然指定entryTypes是一个较老的选项,具有更广泛的浏览器支持,但现在首选使用type,因为它允许指定附加的条目特定的观察配置(例如,下面讨论的buffered标志)。

观察已经发生的条目#

默认情况下,PerformanceObserver对象只能在条目出现时观察条目。如果您希望延迟地加载性能分析代码(以不阻止更高优先级的资源),这可能会有问题。

要获取历史条目(在它们发生之后),请在调用observe()时将buffered标志设置为true。第一次调用PerformanceObserver回调时,浏览器将包含其性能输入缓冲区中的历史条目。

po.observe({
  type: 'some-entry-type',
  buffered: true,
});

为了避免内存问题,性能输入缓冲区不是无限的。对于大多数典型的页面加载,缓冲区不太可能被填满,条目将丢失。

要避免的遗留性能API#

在Performance Observer API之前,开发人员可以使用在Performance对象上定义的以下三种方法访问性能条目:

虽然这些api仍然受支持,但不建议使用它们,因为它们不允许您监听何时发出新条目。此外,许多新的api(如长任务)不是通过performance对象公开的,而是通过PerformanceObserver公开的。

除非您特别需要Internet Explorer兼容性,否则最好在代码中避免使用这些方法,并继续使用PerformanceObserver

用户计时API#

用户计时API是用于基于时间的度量的通用度量API。它允许您任意标记时间点,然后测量这些标记之间的持续时间。

// 在运行任务前立即记录时间。
performance.mark('myTask:start');
await doMyTask();
// 运行任务后立即记录时间。
performance.mark('myTask:end');

// 测量任务开始和结束之间的增量
performance.measure('myTask', 'myTask:start', 'myTask:end');

而API像Date.now()或performance.now()为您提供类似的功能,使用用户计时API的好处是它与性能工具集成得很好。例如,Chrome DevTools在Performance面板中可视化用户计时测量,许多分析提供商也会自动跟踪您所做的任何测量,并将持续时间数据发送到他们的分析后端。

要报告用户计时度量,可以使用PerformanceObserver和register来观察类型为measure的条目:

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 记录条目和所有相关的详细信息。
      console.log(entry.toJSON());
    }
  });
  // 开始监听要调度的“measure”条目。
  po.observe({type: 'measure', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

Long Tasks API

long tasks api有助于了解浏览器的主线程何时被阻塞到足以影响帧速率或输入延迟的时间。目前,API将报告任何执行时间超过50毫秒(ms)的任务。

任何时候你需要运行昂贵的代码(或者加载并执行大型脚本),跟踪这些代码是否阻塞了主线程是很有用的。事实上,许多更高级别的度量是建立在长任务API本身之上的(比如交互时间(TTI)总阻塞时间(TBT))。

要确定长任务何时发生,可以使用PerformanceObserver和register来观察longtask类型的条目:

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 记录条目和所有相关的详细信息。
      console.log(entry.toJSON());
    }
  });
  // 开始监听要调度的“longtask”条目。
  po.observe({type: 'longtask', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

Element Timing API

最大内容绘制(LCP)度量对于了解最大图像或文本块何时绘制到屏幕上非常有用,但在某些情况下,您需要测量不同元素的渲染时间。

对于这些情况,可以使用元素计时API。实际上,最大的Contentful Paint API实际上构建在元素计时API之上,并添加了最大内容元素的自动报告,但是您可以通过显式地向其他元素添加elementtiming属性并注册PerformanceObserver来观察元素条目类型来报告其他元素。

<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
...
<script>
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // Create the performance observer.
  const po = new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
      // Log the entry and all associated details.
      console.log(entry.toJSON());
    }
  });
  // Start listening for `element` entries to be dispatched.
  po.observe({type: 'element', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}
</script>

抓住了! 考虑用于最大内容绘制的元素类型与通过元素计时API观察到的元素类型相同。如果将elementtiming属性添加到不属于这些类型的元素中,则该属性将被忽略。

Event Timing API (事件计时API)

First Input Delay(FirstInputDelay,FID)度量从用户第一次与页面交互到浏览器实际能够开始处理事件处理程序以响应该交互的时间。但是,在某些情况下,测量事件处理时间本身以及直到下一帧可以呈现的时间也可能有用。

使用事件计时API(用于测量FID)可以实现这一点,因为它在事件生命周期中公开了许多时间戳,包括:

  • startTime:浏览器接收事件时的时间。
  • processingStart:浏览器能够开始处理事件的事件处理程序时的时间。
  • processingEnd:浏览器完成执行从事件处理程序为此事件启动的所有同步代码的时间。
  • duration:从浏览器接收事件到完成执行从事件处理程序启动的所有同步代码后能够绘制下一帧的时间(出于安全原因,四舍五入为8毫秒)。

以下示例显示如何使用这些值创建自定义测量:

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  const po = new PerformanceObserver((entryList) => {
    const firstInput = entryList.getEntries()[0];

    // 测量第一次输入延迟(FID)。
    const firstInputDelay = firstInput.processingStart - firstInput.startTime;

    // 测量运行所有事件处理程序所需的时间
    // 注意:这不包括使用异步调度的工作
    // 像 `requestAnimationFrame()` 或者 `setTimeout()`.
    const firstInputProcessingTime = firstInput.processingEnd - firstInput.processingStart;

    //从接收到输入开始测量事件的整个持续时间

	//直到下一帧的浏览器处理完后才可以画画

	//事件处理程序。

	//注:与上述类似,此值不包括计划的工时

	//异步使用“requestAnimationFrame()”或“setTimeout()”。

	//出于安全原因,此值四舍五入到最接近的8毫秒。
    const firstInputDuration = firstInput.duration;

    // 在控制台中记录这些值。
    console.log({
      firstInputDelay,
      firstInputProcessingTime,
      firstInputDuration,
    });
  });

  po.observe({type: 'first-input', buffered: true});
} catch (error) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

Resource Timing API(资源定时API)

资源计时API使开发人员能够详细了解特定页面的资源是如何加载的。尽管API的名称是什么,但它提供的信息不仅限于计时数据(尽管有很多这样的信息)。您可以访问的其他数据包括:

  • initiatorType:如何获取资源:例如从<script><link>标记,或从fetch()

  • nextHopProtocol:用于获取资源的协议,例如h2quic

  • encodedBodySize/decodedBodySize:编码或解码形式的资源大小(分别)

  • transferSize:通过网络实际传输的资源的大小。当资源通过缓存实现时,这个值可能比encodedBodySize小得多,在某些情况下它可以是零(如果不需要缓存重新验证)。

注意,您可以使用资源计时项的transferSize属性来度量缓存命中率指标,甚至可能是总缓存资源大小指标,这可能有助于了解资源缓存策略如何影响重复访问者的性能。

下面的示例记录页面请求的所有资源,并指示每个资源是否通过缓存实现。

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 如果transferSize为0,则通过缓存实现资源。
      console.log(entry.name, entry.transferSize === 0);
    }
  });
  // 开始监听要调度的“resource”条目。
  po.observe({type: 'resource', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

Navigation Timing API(导航计时API)

导航计时API类似于资源计时API,但它只报告导航请求。导航条目类型也类似于resource条目类型,但是它只包含一些特定于导航请求的附加信息(例如当DOMContentLoaded和load事件触发时)。

许多开发人员跟踪了解服务器响应时间(从第一个字节到第一个字节的时间)的一个指标是通过导航计时API(特别是它的条目的responseStart时间戳)获得的。

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 如果transferSize为0,则通过缓存实现资源。
      console.log('Time to first byte', entry.responseStart);
    }
  });
  // 开始监听要调度的“navigation”条目。
  po.observe({type: 'navigation', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作
}

使用ServiceWorker的开发人员可能关心的另一个度量是导航请求的服务工作线程启动时间。这是浏览器在开始拦截获取事件之前启动服务工作线程所用的时间。

特定导航请求的服务工作线程启动时间可以根据entry.responseStart以及entry.workerStart.

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('Service Worker startup time:',
          entry.responseStart - entry.workerStart);
    }
  });
  // 开始监听要调度的“navigation”条目。
  po.observe({type: 'navigation', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}

Server Timing API(服务器计时API)

服务器计时API允许您通过响应头将特定于请求的定时数据从服务器传递到浏览器。例如,您可以指示在数据库中为特定请求查找数据所用的时间,这对于调试由服务器上速度慢引起的性能问题非常有用。

对于使用第三方分析提供商的开发人员,服务器计时API是将服务器性能数据与这些分析工具可能正在测量的其他业务指标关联起来的唯一方法。

若要在响应中指定服务器计时数据,可以使用Server-Timing响应标头。这里有一个例子。

HTTP/1.1 200 OK

Server-Timing: miss, db;dur=53, app;dur=47.2

然后,从页面中,您可以从资源计时和导航计时api中读取有关resourcenavigation 条目的数据。

// 捕获某些浏览器在使用新的“type”选项时引发的错误。
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  // 创建性能观察者。
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // 记录此响应的所有服务器计时数据
      console.log('Server Timing', entry.serverTiming);
    }
  });
  // 开始监听要调度的“navigation”条目。
  po.observe({type: 'navigation', buffered: true});
} catch (e) {
  // 如果浏览器不支持此API,则不执行任何操作。
}