了解使用 Navigation Timing API 和 Resource Timing API 在实际环境中评估加载性能的基础知识。
发布时间:2021 年 10 月 8 日
如果您曾在浏览器开发者工具(或 Chrome 中的 Lighthouse)的网络面板中使用连接限制来评估加载性能,就会知道这些工具在性能调整方面有多么方便。您可以使用一致且稳定的基准连接速度快速衡量性能优化带来的影响。唯一的问题是,这是合成测试,会生成实验室数据,而不是实际环境数据。
合成测试本身并不 差,但它并不能代表您的网站在真实用户面前的加载速度。这需要实际环境数据,您可以从 Navigation Timing API 和 Resource Timing API 收集这些数据。
可帮助您在实际环境中评估加载性能的 API
Navigation Timing 和 Resource Timing 是两个相似的 API,它们有很大的重叠,但衡量的是两个不同的方面:
- Navigation Timing 衡量的是 HTML 文档请求(即导航请求)的速度。
- Resource Timing 衡量的是文档依赖型资源(例如 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 提供的信息量可能非常大,但它们是您在实际环境中衡量加载性能的关键,因为您可以在用户访问您的网站时收集这些计时。
网络请求的生命周期和计时
收集和分析导航和资源计时有点像考古,因为您是在事后重建网络请求的短暂生命周期。有时,直观呈现概念会有所帮助,而对于网络请求,浏览器的开发者工具可以提供帮助。
网络请求的生命周期有不同的阶段,例如 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;
连接协商
影响加载性能的另一个因素是连接协商,即连接到 Web 服务器时产生的延迟。如果涉及 HTTPS,此过程还将包括 TLS 协商时间。连接阶段包含三个计时:
connectStart是浏览器开始打开与 Web 服务器的连接的时间。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标记浏览器开始提取资源(Resource Timing)或导航请求的文档(Navigation Timing)的时间。这先于实际请求,是浏览器检查缓存(例如 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;
您可以进行的其他衡量
Navigation Timing 和 Resource Timing 的用途不仅仅是前面的示例中介绍的那些。以下是一些其他情况,其中包含可能值得探索的相关计时:
- 页面重定向 :重定向是一个被忽视的延迟来源,尤其是重定向链。延迟会以多种方式增加,例如 HTTP 到 HTTPS 的跃点,以及 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 的详尽资源,而只是一个起点。以下是一些您可能会觉得有用的其他资源:
- Navigation Timing 规范。
- Resource Timing 规范。
- ResourceTiming 实践。
- Navigation Timing API (MDN)
- Resource Timing API (MDN)
借助这些 API 及其提供的数据,您将能够更好地了解真实用户体验到的加载性能,从而更有信心诊断和解决实际环境中的加载性能问题。