自定义指标

拥有可在任何给定网站上普遍衡量的以用户为中心的指标非常有价值。通过这些指标,您可以:

  • 了解真实用户对整个网络的体验。
  • 将您的网站与竞争对手的网站进行比较。
  • 在分析工具中跟踪实用且富有实用价值的数据,而无需编写自定义代码。

通用指标是一个很好的基准,但在许多情况下,您需要衡量的不仅仅是这些指标,还需要衡量更多指标,才能全面了解特定网站的体验。

借助自定义指标,您可以衡量可能仅适用于您网站的网站体验方面,例如:

  • 单页应用 (SPA) 从一个“页面”转换到另一个“页面”所需的时间。
  • 网页为已登录用户显示从数据库提取的数据所需的时间。
  • 服务器端渲染 (SSR) 应用的补充所需的时间。
  • 回访者加载的资源的缓存命中率。
  • 游戏中点击或键盘事件的事件延迟时间。

用于衡量自定义指标的 API

过去,Web 开发者没有太多用于衡量性能的低级 API,因此他们不得不使用一些权宜方法来衡量网站的性能。

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

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

Performance Observer API

浏览器支持

  • Chrome:52.
  • Edge:79。
  • Firefox:57.
  • Safari:11.

来源

Performance Observer API 是一种机制,用于收集和显示本页中所述的所有其他性能 API 中的数据。了解它对于获得良好的数据至关重要。

您可以使用 PerformanceObserver 被动订阅与性能相关的事件。这样,API 回调便会在空闲时段触发,这意味着它们通常不会干扰网页性能。

如需创建 PerformanceObserver,请向其传递一个回调,以便在每次调度新的效果条目时运行该回调。然后,您可以使用 observe() 方法告知观察器要监听哪些类型的条目:

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'});

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

观察已发生的条目

默认情况下,PerformanceObserver 对象只能在有条目发生时观察这些条目。如果您想延迟加载效果分析代码,以免其阻塞优先级更高的资源,这可能会导致问题。

如需获取历史条目(在发生后),请在调用 observe() 时将 buffered 标志设置为 true。在首次调用 PerformanceObserver 回调时,浏览器会包含其效果条目缓冲区中的历史条目,最多不超过该类型的缓冲区大小上限

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

应避免使用哪些旧版性能 API

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

虽然这些 API 仍然受支持,但我们不建议使用它们,因为它们不允许您监听何时发出新条目。此外,许多新 API(例如 largest-contentful-paint)并非通过 performance 对象公开,而是仅通过 PerformanceObserver 公开。

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

User Timing API

浏览器支持

  • Chrome:28.
  • Edge:12.
  • Firefox:38.
  • Safari:11.

来源

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 会在“性能”面板中直观呈现用户时间测算结果,许多分析服务提供商还会自动跟踪您进行的任何测算,并将时长数据发送到其分析后端。

如需报告 User Timing 测量结果,您可以使用 PerformanceObserver 并注册以观察类型为 measure 的条目:

// 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});

Long Tasks API

浏览器支持

  • Chrome:58.
  • Edge:79。
  • Firefox:不受支持。
  • Safari:不受支持。

来源

Long Tasks API 有助于了解浏览器的主线程何时被阻塞的时间足够长,以影响帧速率或输入延迟。该 API 会报告任何执行时间超过 50 毫秒的任务。

每当您需要运行耗时代码或加载和执行大型脚本时,跟踪相应代码是否会阻塞主线程都很有用。事实上,许多更高级别的指标都是基于 Long Tasks API 本身构建的(例如 Time to Interactive (TTI)Total Blocking Time (TBT))。

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

// 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});

Long Animation Frames API

浏览器支持

  • Chrome:123。
  • Edge:123。
  • Firefox:不受支持。
  • Safari:不受支持。

来源

Long Animation Frames API 是 Long Tasks API 的一次新迭代,用于查看超过 50 毫秒的长帧(而非长任务)。这解决了 Long Tasks API 的一些缺点,包括更好地归因和更广泛的可能存在问题的延迟范围。

如需确定出现长帧的时间,您可以使用 PerformanceObserver 并注册以观察类型为 long-animation-frame 的条目:

// 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 `long-animation-frame` entries to be dispatched.
po.observe({type: 'long-animation-frame', buffered: true});

Element Timing API

浏览器支持

  • Chrome:77.
  • Edge:79。
  • Firefox:不受支持。
  • Safari:不受支持。

来源

Largest Contentful Paint (LCP) 指标有助于了解何时将最大的图片或文本块绘制到屏幕上,但在某些情况下,您可能需要衡量其他元素的渲染时间。

对于这些情况,请使用 Element Timing API。LCP API 实际上是基于 Element Timing API 构建的,并会自动报告包含最多内容的元素,但您也可以通过向其他元素显式添加 elementtiming 属性并注册 PerformanceObserver 来观察 element 条目类型,从而针对其他元素生成报告。

<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
<!-- ... -->

<script>
  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});
</script>

Event Timing API

浏览器支持

  • Chrome:76.
  • Edge:79。
  • Firefox:89.
  • Safari:不受支持。

来源

Interaction to Next Paint (INP) 指标通过观察网页生命周期内的所有点击、点按和键盘互动,评估网页的总体响应速度。网页的 INP 通常是指从用户发起互动到浏览器绘制显示用户输入的视觉结果的下一帧之间的用时,即完成互动所用时间最长的那次互动。

INP 指标由 Event Timing API 提供支持。此 API 会公开事件生命周期期间发生的多个时间戳,包括:

  • startTime:浏览器收到事件的时间。
  • processingStart:浏览器能够开始处理事件的事件处理脚本的时间。
  • processingEnd:浏览器完成执行此事件的事件处理脚本发起的所有同步代码的时间。
  • duration:从浏览器收到事件到完成从事件处理脚本发起的所有同步代码的执行并能够绘制下一帧之间的时间(出于安全考虑,会舍入到 8 毫秒)。

以下示例展示了如何使用这些值创建自定义衡量:

const po = new PerformanceObserver((entryList) => {
  // Get the last interaction observed:
  const entries = Array.from(entryList.getEntries()).forEach((entry) => {
    // Get various bits of interaction data:
    const inputDelay = entry.processingStart - entry.startTime;
    const processingTime = entry.processingEnd - entry.processingStart;
    const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
    const duration = entry.duration;
    const eventType = entry.name;
    const target = entry.target || "(not set)"

    console.log("----- INTERACTION -----");
    console.log(`Input delay (ms): ${inputDelay}`);
    console.log(`Event handler processing time (ms): ${processingTime}`);
    console.log(`Presentation delay (ms): ${presentationDelay}`);
    console.log(`Total event duration (ms): ${duration}`);
    console.log(`Event type: ${eventType}`);
    console.log(target);
  });
});

// A durationThreshold of 16ms is necessary to include more
// interactions, since the default is 104ms. The minimum
// durationThreshold is 16ms.
po.observe({type: 'event', buffered: true, durationThreshold: 16});

Resource Timing API

浏览器支持

  • Chrome:29.
  • Edge:12.
  • Firefox:35.
  • Safari:11.

来源

借助 Resource Timing API,开发者可以详细了解特定网页的资源加载方式。尽管该 API 的名称是 Timing,但它提供的信息不仅限于时间数据(尽管有大量时间数据)。您可以访问的其他数据包括:

  • initiatorType:提取资源的方式:例如从 <script><link> 标记中提取,或从 fetch() 调用中提取。
  • nextHopProtocol:用于提取资源的协议,例如 h2quic
  • encodedBodySize/decodedBodySize]:资源以编码或解码形式(分别)的大小
  • transferSize:实际通过网络传输的资源大小。当资源由缓存提供时,此值可能远小于 encodedBodySize,在某些情况下,此值甚至可以为零(如果不需要重新验证缓存)。

您可以使用资源时间记录条目的 transferSize 属性来衡量缓存命中率指标或缓存的总资源大小指标,这有助于了解您的资源缓存策略对回访者的性能有何影响。

以下示例会记录网页请求的所有资源,并指明缓存是否满足了每个资源。

// Create the performance observer.
const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // If transferSize is 0, the resource was fulfilled using the cache.
    console.log(entry.name, entry.transferSize === 0);
  }
});

// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});

浏览器支持

  • Chrome:57。
  • Edge:12.
  • Firefox:58.
  • Safari:15.

来源

Navigation Timing API 与 Resource Timing API 类似,但它仅报告导航请求navigation 条目类型也类似于 resource 条目类型,但它包含一些仅适用于导航请求的附加信息(例如 DOMContentLoadedload 事件触发时)。

许多开发者跟踪的一个指标是服务器响应时间(首字节时间 [TTFB]),可通过 Navigation Timing API 获取,具体而言,就是条目的 responseStart 时间戳。

// Create the performance observer.
const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // If transferSize is 0, the resource was fulfilled using the cache.
    console.log('Time to first byte', entry.responseStart);
  }
});

// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});

使用 Service Worker 的开发者可能还会关心导航请求的 Service Worker 启动时间。这是浏览器启动 Service Worker 线程所需的时间,之后才能开始拦截 fetch 事件。

可以根据 entry.responseStartentry.workerStart 之间的差值确定特定导航请求的服务工作器启动时间。

// Create the performance observer.
const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('Service Worker startup time:',
        entry.responseStart - entry.workerStart);
  }
});

// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});

Server Timing API

浏览器支持

  • Chrome:65.
  • Edge:79。
  • Firefox:61.
  • Safari:16.4。

来源

借助 Server Timing API,您可以通过响应标头将请求专用的时间数据从服务器传递到浏览器。例如,您可以指明为特定请求在数据库中查找数据所花费的时间,这对于调试由服务器运行缓慢导致的性能问题非常有用。

对于使用第三方分析服务提供商的开发者,Server Timing API 是将服务器性能数据与这些分析工具可能衡量的其他业务指标相关联的唯一方式。

如需在响应中指定服务器时间数据,您可以使用 Server-Timing 响应标头。示例如下:

HTTP/1.1 200 OK

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

然后,您可以在网页中通过 Resource Timing API 和 Navigation Timing API 的 resourcenavigation 条目读取这些数据。

// Create the performance observer.
const po = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Logs all server timing data for this response
    console.log('Server Timing', entry.serverTiming);
  }
});

// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});