自定义指标

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

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

通用指标提供了一个很好的基准,但在许多情况下,除了这些指标之外,您还需要衡量更多指标,以便获得特定网站的完整体验。

通过自定义指标,您可以衡量网站体验中可能仅适用于您网站的各个方面,例如:

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

用于衡量自定义指标的 API

过去,网络开发者没有太多的低级 API 来衡量性能,因此他们不得不依靠黑客手段来衡量网站是否表现良好。

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

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

Performance Observer API

浏览器支持

  • 52
  • 79
  • 57
  • 11

来源

Performance Observer API 是一种机制,用于收集和显示来自本页讨论的所有其他 Performance 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 属性检查可用的条目类型。

观察已经发生的条目

默认情况下,PerformanceObserver 对象只能在出现条目时观察它们。如果您想延迟加载性能分析代码,以免阻塞优先级较高的资源,就可能会导致问题。

如需获取历史条目(在它们出现之后),请在调用 observe() 时将 buffered 标志设置为 true。首次调用 PerformanceObserver 回调时,浏览器将包含其性能条目缓冲区中的历史条目。

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

要避免的旧版性能 API

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

如需报告 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

浏览器支持

  • 58
  • 79
  • x
  • x

来源

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

每当您需要运行开销大的代码,或者加载和执行大型脚本时,跟踪这些代码是否会阻塞主线程都非常有用。事实上,许多更高级别的指标都是基于 Long Tasks API 本身构建的,例如可交互时间 (TTI)总阻塞时间 (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

浏览器支持

  • 77
  • 79
  • x
  • x

来源

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>
  // 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>

Event Timing API

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

您可以通过 Event Timing API 使用 INP 指标。此 API 公开了事件生命周期中发生的许多时间戳,包括:

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

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

// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
  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 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 time (ms): ${processingTime}`);
      console.log(`Total event duration (ms): ${duration}`);
      console.log(`Event type: ${eventType}`);
      console.log(target);
    });
  });

  // A durationThreshold of 16ms is necessary to surface more
  // interactions, since the default is 104ms. The minimum
  // durationThreshold is 16ms.
  po.observe({type: 'event', buffered: true, durationThreshold: 16});
} catch (error) {
  // Do nothing if the browser doesn't support this API.
}

Resource Timing API

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

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

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

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

// 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()) {
      // If transferSize is 0, the resource was fulfilled via the cache.
      console.log(entry.name, entry.transferSize === 0);
    }
  });

  // Start listening for `resource` entries to be dispatched.
  po.observe({type: 'resource', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

浏览器支持

  • 57
  • 12
  • 58
  • 15

来源

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

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

// 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()) {
      // If transferSize is 0, the resource was fulfilled via the cache.
      console.log('Time to first byte', entry.responseStart);
    }
  });

  // Start listening for `navigation` entries to be dispatched.
  po.observe({type: 'navigation', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

使用 Service Worker 的开发者可能关心的另一个指标是导航请求的 Service Worker 启动时间。这是浏览器在开始拦截提取事件之前启动 Service Worker 线程所用的时间。

特定导航请求的 Service Worker 启动时间可以根据 entry.responseStartentry.workerStart 之间的增量确定。

// 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()) {
      console.log('Service Worker startup time:',
          entry.responseStart - entry.workerStart);
    }
  });

  // Start listening for `navigation` entries to be dispatched.
  po.observe({type: 'navigation', buffered: true});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}

Server Timing API

利用 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 条目的此类数据。

// 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()) {
      // 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});
} catch (e) {
  // Do nothing if the browser doesn't support this API.
}