了解使用 Navigation 和 Resource Timing API 评估现场加载性能的基础知识。
发布时间:2021 年 10 月 8 日
如果您使用过浏览器开发者工具中的“网络”面板(或 Chrome 中的 Lighthouse)中的连接节流功能来评估加载性能,您就会知道这些工具在进行性能优化方面有多方便。您可以使用一致且稳定的基准连接速度来快速衡量性能优化的影响。唯一的问题是,这是合成测试,生成的是实验室数据,而不是实测数据。
模拟测试本身并不不好,但不能代表您的网站在向真实用户加载时的速度。这需要字段数据,您可以通过 Navigation Timing API 和 Resource Timing API 收集这些数据。
可帮助您评估实际加载性能的 API
Navigation Timing 和 Resource Timing 是两个类似的 API,存在明显的重叠,衡量两种不同的方面:
这些 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 提供的信息量可能非常庞大,但却是衡量实际加载性能的关键,因为您可以在用户访问您的网站时从他们那里收集这些计时数据。
网络请求的生命周期和时间
收集和分析导航和资源时间有点像考古,因为您是在事后重建网络请求的短暂生命周期。有时,它有助于直观呈现概念,而对于网络请求而言,浏览器的开发者工具可以提供帮助。
网络请求的生命周期有不同的阶段,例如 DNS 查找、连接建立、TLS 协商和其他延迟来源。这些时间戳表示为 DOMHighResTimestamp
。时间精确度可能精确到微秒,也可能向上舍入到毫秒,具体取决于您使用的浏览器。您需要详细研究这些阶段,以及它们与 Navigation Timing 和 Resource Timing 之间的关系。
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
是指浏览器开始打开与网络服务器的连接。secureConnectionStart
标记客户端开始 TLS 协商的时间。connectEnd
表示已建立与 Web 服务器的连接。
衡量总连接时间与衡量总 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
用于标记何时开始在 Service Worker 的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 到 HTTP 的跃点以及 302/未缓存的 301 重定向。
redirectStart
、redirectEnd
和redirectCount
时间戳有助于评估重定向延迟时间。 - 文档卸载:在运行
unload
事件处理脚本中代码的页面中,浏览器必须先执行该代码,然后才能导航到下一个页面。unloadEventStart
和unloadEventEnd
用于测量文档卸载。 - 文档处理:除非您的网站发送非常大的 HTML 有效负载,否则文档处理时间可能并不重要。如果您遇到的是这种情况,建议您选择
domInteractive
、domContentLoadedEventStart
、domContentLoadedEventEnd
和domComplete
时间。
如何在代码中获取时间
到目前为止,所有显示的示例都使用了 performance.getEntriesByType
,但您还可以通过其他方式查询性能条目缓冲区,例如 performance.getEntriesByName
和 performance.getEntries
。如果只需要浅度分析,这些方法很合适。但在其他情况下,它们可能会通过迭代大量条目,甚至重复轮询性能缓冲区来查找新条目,从而引入过多的主线程工作。
如需从性能条目缓冲区收集条目,建议使用 PerformanceObserver
。PerformanceObserver
会监听性能条目,并在向缓冲区添加这些条目时提供这些条目:
// 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 百分位数或更高百分位数的体验,就是将重点放在最慢的体验上。
本指南并非关于导航或资源时间的详尽资源,而是起到一个起点的作用。下面是一些可能对您有用的其他资源:
- Navigation Timing 规范。
- Resource Timing Spec。
- ResourceTiming 的实际运用。
- Navigation Timing API (MDN)
- Resource Timing API (MDN)
借助这些 API 及其提供的数据,您将更有能力了解真实用户的加载性能体验,从而更有信心地诊断和解决现场加载性能问题。