[TOC]

以用户为中心的指标具有很大价值,从普遍意义上,通过它们可以衡量任何给定网站。这些指标允许您:

  • 了解真实用户对 Web 的整体体验如何
  • 轻松比较您的网站与竞争对手网站
  • 在分析工具中跟踪有用且可操作的数据,无需编写自定义代码

通用指标提供了一个很好的基线,但在许多情况下,您需要测量更多指标,才能刻画出具体网站的完整体验。

自定义指标允许您衡量可能仅适用于您的网站的使用体验的各个方面,例如:

  • 单页面应用程序 (SPA) 从一“页”转换到另一“页”需要多长时间
  • 页面针对登录用户显示从数据库获取的数据需要多长时间
  • 服务器端渲染 (SSR) 应用程序需要多长时间才能注水
  • 回访者加载资源的缓存命中率
  • 游戏中点击或键盘事件的事件延迟

用于测量自定义指标的 API

从历史上看,Web 开发人员没有很多低级 API 可用来衡量性能,因此他们不得不采用一些修改技巧才能衡量网站的性能是否良好。

例如,可以运行 requestAnimationFrame 循环并计算相邻帧之间的增量时间,来确定主线程是否由于长时间运行 JavaScript 任务而被阻塞。如果增量时间明显长于显示帧率,则可以将其报告为一个长任务。但是,不建议使用此类修改技巧,因为实际上它们本身就会影响性能(例如,耗尽电池电量)。

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

性能观察器

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

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

创建 PerformanceObserver 的方式是,向其传递一个只要分派新的性能条目就会运行的回调。然后告诉该观察器要通过 observe() 方法监听哪些类型的条目:

// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  const po = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // Log the entry and all associated details.
      console.log(entry.toJSON());
    }
  });

  po.observe({type: 'some-entry-type'});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

以下各部分列出了所有可进行观察的不同条目类型,但在较新的浏览器中,您可以通过静态 PerformanceObserver.supportedEntryTypes 属性检查哪些条目类型可用。

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

观察已经发生的条目

默认情况下, PerformanceObserver 对象只能在条目发生时观察它们。如果您想要延迟加载性能分析代码(为了不阻止优先级更高的资源),这可能会出现问题。

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

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

为避免出现内存问题,性能条目缓冲区不是无限制的。对于大多数典型的页面加载,不太可能出现缓冲区填满和丢失条目的情况。

要避免使用的旧版性能 API

在性能观察器 API 之前,开发人员可以使用 performance 对象中定义的以下三种方法访问性能条目:

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

除非您明确需要 Internet Explorer 兼容性,否则最好在代码中避免使用这些方法,而是使用 PerformanceObserver

User Timing API

User Timing API 是基于时间的指标的通用测量 API。它允许您任意标记时间点,然后测量这些标记之间的时长。

// Record the time immediately before running a task.
performance.mark('myTask:start');
await doMyTask();
// Record the time immediately after running a task.
performance.mark('myTask:end');

// Measure the delta between the start and end of the task
performance.measure('myTask', 'myTask:start', 'myTask:end');

虽然 Date.now()performance.now() 等 API 可以提供相似功能,但使用 User Timing API 的好处是它可以与性能工具很好地集成。例如,Chrome DevTools 可以使 Performance 面板中的 User Timing 测量值可视化,而且许多分析提供商还会自动跟踪您所做的任何测量,并将时长数据发送到他们的分析后端。

要报告 User Timing 测量值,您可以使用 PerformanceObserver 并注册以观察 measure 类型的条目:

// 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((list) => {
    for (const entry of list.getEntries()) {
      // Log the entry and all associated details.
      console.log(entry.toJSON());
    }
  });
  // Start listening for `measure` entries to be dispatched.
  po.observe({type: 'measure', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

Long Tasks API

Long Tasks API 用于了解浏览器主线程的阻塞时间何时长到足以影响帧率或输入延迟。目前,该 API 将报告执行时间超过 50 毫秒 (ms) 的任何任务。

任何时候您需要运行开销大的代码(或加载和执行大型脚本)时,跟踪该代码是否阻塞主线程都是很有用的。事实上,许多较高级别的指标都是建立在 Long Tasks API 上的(例如 Time to Interactive (TTI)Total Blocking Time (TBT))

要确定何时发生长任务,您可以使用 PerformanceObserver 并注册以观察 longtask 类型的条目:

// 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((list) => {
    for (const entry of list.getEntries()) {
      // Log the entry and all associated details.
      console.log(entry.toJSON());
    }
  });
  // Start listening for `longtask` entries to be dispatched.
  po.observe({type: 'longtask', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

Element Timing API

Largest Contentful Paint (LCP) 指标用于了解最大的图像或文本块何时绘制到屏幕上,但在某些情况下,您希望测量不同元素的渲染时间。

对于这些情况,您可以使用 Element Timing API。实际上,Largest Contentful Paint API 建立在 Element Timing 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>

针对 Largest Contentful Paint 考虑的元素类型与可通过 Element Timing API 观察的元素类型相同。如果您将 elementtiming 属性添加到不属于这些类型的元素,该属性将被忽略。

Event Timing API #

First Input Delay (FID) 指标测量从用户首次与页面交互到浏览器实际能够开始处理事件处理程序以响应该交互的时间。但是,在某些情况下,测量事件处理时间本身以及直到可以渲染下一帧之前的时间也可能很有用。
这可以通过 Event Timing API(用于测量 FID)实现,因为它公开了事件生命周期中的许多时间戳,包括:

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

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

// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  const po = new PerformanceObserver((entryList) => {
    const firstInput = entryList.getEntries()[0];

    // Measure First Input Delay (FID).
    const firstInputDelay = firstInput.processingStart - firstInput.startTime;

    // Measure the time it takes to run all event handlers
    // Note: this does not include work scheduled asynchronously using
    // methods like `requestAnimationFrame()` or `setTimeout()`.
    const firstInputProcessingTime = firstInput.processingEnd - firstInput.processingStart;

    // Measure the entire duration of the event, from when input is received by
    // the browser until the next frame can be painted after processing all
    // event handlers.
    // Note: similar to above, this value does not include work scheduled
    // asynchronously using `requestAnimationFrame()` or `setTimeout()`.
    // And for security reasons, this value is rounded to the nearest 8ms.
    const firstInputDuration = firstInput.duration;

    // Log these values the console.
    console.log({
      firstInputDelay,
      firstInputProcessingTime,
      firstInputDuration,
    });
  });

  po.observe({type: 'first-input', buffered: true});
} catch (error) {
  // Do nothing if the browser doesn't support this API.
}

Resource Timing API #

Resource Timing API 使开发人员可以详细了解特定页面的资源是如何加载的。尽管该 API 的名称如此,但它提供的信息不局限于计时数据(虽然有大量该数据)。您可以访问的其他数据包括:

  • initiatorType:资源的获取方式:例如从