如何使用 Navigation Timing 和 Resource Timing 评估现场加载性能

了解使用 Navigation 和 Resource Timing API 评估现场加载性能的基础知识。

发布时间:2021 年 10 月 8 日

如果您曾在浏览器开发者工具的“Network”面板(或 Chrome 中的 Lighthouse)中使用连接节流功能来评估加载性能,那么您一定知道这些工具在性能调优方面有多么方便。您可以通过一致且稳定的基准连接速度,快速衡量性能优化的效果。唯一的问题是,这项测试是合成测试,会产生实验室数据,而不是现场数据

模拟测试本身并不不好,但不能代表您的网站在向真实用户加载时的速度。这需要现场数据,您可以通过 Navigation Timing API 和 Resource Timing API 收集这些数据。

Navigation Timing 和 Resource Timing 是两个类似的 API,具有大量重叠的测量内容,分别衡量以下两项内容:

  • 导航时间用于衡量 HTML 文档请求(即导航请求)的速度。
  • 资源时间用于衡量对文档依赖的资源(例如 CSS、JavaScript、图片和其他资源类型)的请求速度。

这些 API 会在性能条目缓冲区中公开其数据,您可以通过 JavaScript 在浏览器中访问该缓冲区。您可以通过多种方式查询性能缓冲区,但常用的方式是使用 performance.getEntriesByType

// Get Navigation Timing entries:
performance.getEntriesByType('navigation');

// Get Resource Timing entries:
performance.getEntriesByType('resource');

performance.getEntriesByType 接受一个字符串,该字符串描述您要从性能条目缓冲区检索的条目类型。'navigation''resource' 分别用于检索 Navigation Timing API 和 Resource Timing API 的计时信息。

这些 API 提供的信息量可能非常庞大,但它们是您衡量现场加载性能的关键,因为您可以从用户访问您的网站时收集这些时间信息。

网络请求的生命周期和时间

收集和分析导航和资源时间有点像考古,因为您是在事后重建网络请求的短暂生命周期。有时,直观呈现概念会很有帮助。对于网络请求,浏览器的开发者工具会很有用。

Chrome 的 DevTools 中显示的网络时间。图中显示的时间是请求队列、连接协商、请求本身和响应的颜色条。
Chrome DevTools 的“网络”面板中显示的网络请求的可视化结果

网络请求的生命周期有不同的阶段,例如 DNS 查找、连接建立、TLS 协商和其他延迟来源。这些时间戳表示为 DOMHighResTimestamp。根据浏览器的不同,计时的粒度可能精确到微秒,也可能向上舍入到毫秒。您需要详细研究这些阶段,以及它们与导航时间和资源时间的关系。

DNS 查找

当用户访问某个网址时,系统会查询域名系统 (DNS),以将域名转换为 IP 地址。此过程可能需要很长时间,您可能需要在现场测量时间。Navigation Timing 和 Resource Timing 公开了两种与 DNS 相关的计时:

  • domainLookupStart 是 DNS 查询开始的时间。
  • domainLookupEnd 是 DNS 查询结束的时间。

您可以通过从结束指标中减去开始指标来计算总 DNS 查找时间:

// Measuring DNS lookup time
const [pageNav] = performance.getEntriesByType('navigation');
const totalLookupTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

连接协商

影响加载性能的另一个因素是连接协商,这是连接到网络服务器时产生的延迟。如果涉及 HTTPS,此过程还将包括 TLS 协商时间。连接阶段包括三个时间:

  • connectStart 是指浏览器开始打开与 Web 服务器的连接。
  • secureConnectionStart 标记客户端开始 TLS 协商的时间。
  • connectEnd 表示已建立与网络服务器的连接。

衡量总连接时间与衡量总 DNS 查找时间类似:您只需将结束时间减去开始时间即可。不过,还有一个额外的 secureConnectionStart 属性,如果未使用 HTTPS 或连接是持久连接,该属性的值可能是 0。如果您想衡量 TLS 协商时间,请注意以下几点:

// Quantifying total connection time
const [pageNav] = performance.getEntriesByType('navigation');
const connectionTime = pageNav.connectEnd - pageNav.connectStart;
let tlsTime = 0; // <-- Assume 0 to start with

// Was there TLS negotiation?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

DNS 查找和连接协商结束后,与提取文档及其依赖资源相关的时间就开始起作用了。

请求和响应

加载性能受两类因素的影响:

  • 外部因素:延迟时间和带宽等。除了选择托管公司和可能的 CDN 之外,我们无法控制这些因素,因为用户可以从任何地方访问网站。
  • 固有因素:这些因素包括服务器和客户端架构、资源大小以及我们针对这些因素进行优化的能力,这些都在我们的控制范围内。

这两种因素都会影响加载性能。与这些因素相关的时间非常重要,因为它们描述了资源的下载时间。Navigation Timing 和 Resource Timing 使用以下指标描述了加载性能:

  • fetchStart 标记浏览器开始提取资源(资源时间)或导航请求的文档(导航时间)的时间。这发生在实际请求之前,是浏览器检查缓存(例如 HTTP 和 Cache 实例)的时刻。
  • workerStart 用于标记请求何时开始在服务工作器的 fetch 事件处理程序中处理。如果没有 Service Worker 控制当前页面,此值将为 0
  • requestStart 是浏览器发出请求的时间。
  • responseStart 是响应的第一个字节到达的时间。
  • responseEnd 是响应的最后一个字节到达的时间。

通过这些计时,您可以衡量加载性能的多个方面,例如 Service Worker 中的缓存查找和下载时间:

// Cache seek plus response time of the current document
const [pageNav] = performance.getEntriesByType('navigation');
const fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
let workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

您还可以测量请求和响应延迟时间的其他方面:

const [pageNav] = performance.getEntriesByType('navigation');

// Request time only (excluding redirects, DNS, and connection/TLS time)
const requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
const responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
const requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

您可以进行的其他测量

导航时间和资源时间不仅适用于前面的示例所述的情况。以下是一些其他可能值得探索的相关时间安排:

  • 网页重定向:重定向(尤其是重定向链)是导致延迟时间增加的根源,但被忽视了。延迟时间会通过多种方式增加,例如 HTTP 到 HTTPs 跳转,以及 302/未缓存的 301 重定向。redirectStartredirectEndredirectCount 时间戳有助于评估重定向延迟时间。
  • 文档卸载:在通过 unload 事件处理脚本运行代码的网页中,浏览器必须先执行该代码,然后才能转到下一页。unloadEventStartunloadEventEnd 用于衡量文档卸载。
  • 文档处理:除非您的网站发送的 HTML 载荷非常大,否则文档处理时间可能不会造成太大影响。如果情况符合上述描述,您可能需要了解 domInteractivedomContentLoadedEventStartdomContentLoadedEventEnddomComplete 的计时。

如何在代码中获取时间

到目前为止,所有显示的示例都使用了 performance.getEntriesByType,但您还可以通过其他方式查询性能条目缓冲区,例如 performance.getEntriesByNameperformance.getEntries。如果只需要浅度分析,这些方法很合适。不过,在其他情况下,它们可能会通过迭代大量条目或甚至重复轮询性能缓冲区以查找新条目,而导致主线程工作量过多。

如需从性能条目缓冲区收集条目,建议使用 PerformanceObserverPerformanceObserver 会监听性能条目,并在将其添加到缓冲区时提供这些条目:

// Create the performance observer:
const perfObserver = new PerformanceObserver((observedEntries) => {
  // Get all resource entries collected so far:
  const entries = observedEntries.getEntries();

  // Iterate over entries:
  for (let i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer for Navigation Timing entries:
perfObserver.observe({
  type: 'navigation',
  buffered: true
});

// Run the observer for Resource Timing entries:
perfObserver.observe({
  type: 'resource',
  buffered: true
});

与直接访问性能条目缓冲区相比,这种收集时间的方法可能会让人感觉很尴尬,但最好将主线程与不符合关键且面向用户用途的工作捆绑在一起。

如何拨打回家电话

收集到所需的所有时间后,您可以将其发送到端点以供进一步分析。您可以通过以下两种方式实现此目的:使用 navigator.sendBeacon 或设置了 keepalive 选项的 fetch。这两种方法都会以非阻塞方式向指定端点发送请求,并且请求会以一种在必要时会超出当前网页会话生命周期的方式加入队列:

// Check for navigator.sendBeacon support:
if ('sendBeacon' in navigator) {
  // Caution: If you have lots of performance entries, don't
  // do this. This is an example for illustrative purposes.
  const data = JSON.stringify(performance.getEntries());

  // Send the data!
  navigator.sendBeacon('/analytics', data);
}

在此示例中,JSON 字符串将到达 POST 载荷,您可以根据需要对其进行解码、处理和存储在应用后端。

总结

收集指标后,您需要自行确定如何分析这些现场数据。在分析现场数据时,请遵循以下几项一般规则,以确保得出有意义的结论:

  • 避免使用平均值,因为平均值不能代表任何一位用户的体验,并且可能会因极端值而偏离。
  • 依赖百分位数。在基于时间的性能指标数据集中,数值越低越好。这意味着,当您优先考虑低百分位数时,您只会关注最快的体验。
  • 优先处理长尾值。当您优先考虑位于第 75 百分位数或更高百分位数的体验时,您将把重点放在最慢的体验上。

本指南并非关于导航或资源时间的详尽资源,而是起到一个起点的作用。下面是一些可能对您有用的其他资源:

借助这些 API 及其提供的数据,您将更有能力了解真实用户的加载性能体验,从而更有信心地诊断和解决现场加载性能问题。