页面加载性能之感知真实世界

虽然咱们能够经过开发者工具以及lighthouse等工具来查看网站的加载状况,并按以前咱们说的那些方案作好了优化,但真正用户打开是否真的如预期通常快,咱们不得而知。一直以来咱们都以实验室数据为测试的依据,这些不能表明现场数据,即真实用户的体验。css

RUM(Real User Monitoring)所以而诞生。RUM依赖于浏览器提供的API来搜集真实用户的性能数据,主要包含2个标准文档,Navigation Timing APIResource Timing API,这两个API都是基于 High Resolution Time 的规范定制的。git

本文档将引导你去认识这些API提供的数据,更好的掌握RUM。github

浏览器中的网络请求

Navigation和Resource Timing之间有部分交集,但二者收集的数据指标仍是不同的。web

  • Navigation Timing 收集了HTML文档的性能指标
  • Resource Timing 收集了文档依赖的资源的性能指标,如:css,js,图片等等

先在控制台尝试执行一下如下代码:数组

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

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

getEntriesByType 接收一个字符串参数,表示你要获取的条目类型。想要获取Navigation Timing的条目,则传 navigation,另外一个则是传 resource。以上代码执行结果,能够看到相似下方的对象结构:浏览器

{
  "connectEnd": 152.20000001136214,
  "connectStart": 85.00000007916242,
  "decodedBodySize": 1270,
  "domComplete": 377.90000007953495,
  "domContentLoadedEventEnd": 236.4000000525266,
  "domContentLoadedEventStart": 236.4000000525266,
  "domInteractive": 236.2999999895692,
  "domainLookupEnd": 85.00000007916242,
  "domainLookupStart": 64.4000000320375,
  "duration": 377.90000007953495,
  "encodedBodySize": 606,
  "entryType": "navigation",
  "fetchStart": 61.600000015459955,
  "initiatorType": "navigation",
  "loadEventEnd": 377.90000007953495,
  "loadEventStart": 377.90000007953495,
  "name": "https://example.com/",
  "nextHopProtocol": "h2",
  "redirectCount": 0,
  "redirectEnd": 0,
  "redirectStart": 0,
  "requestStart": 152.50000008381903,
  "responseEnd": 197.80000008177012,
  "responseStart": 170.00000004190952,
  "secureConnectionStart": 105.80000001937151,
  "startTime": 0,
  "transferSize": 789,
  "type": "navigate",
  "unloadEventEnd": 0,
  "unloadEventStart": 0,
  "workerStart": 0
}

上面的数据看起来很晕,但只要记住一点:你在开发者工具中 Network 看到的 waterflow ,就是用这些数据画出来的。你也能够用这些数据绘制相似的图,用一些工具就能作到,Waterfall 或者 Performance-Bookmarklet缓存

用这些API能够分析用户打开一个网站的每个步骤的耗时,你也能够在js中上去使用这些API来收集真实用户的性能数据。服务器

网络请求的生命周期

在你收集完这些性能数据以后,为了更形象的去理解他们,你须要了解一个请求从发起到结束到底经历了什么,开发者工具能够提供这样的图表,以下:网络

如预期的同样,能够看到这些步骤:DNS查询,创建链接,TLS握手等等。接下来咱们会对着这份数据依次去介绍它们。架构

如下纯属主观见解,想要客观地去学习,回到上方提供的对应API的标准文档阅读

DNS查询

DNS全称Domain Name System,简单理解就是根据域名查询对应的IP地址。取决于你中间的DNS代理层数,可能会花费一些时间。Navigation和Resource Timing都包含如下2个和DNS查询相关的属性:

  • domainLookupStart 表明DNS开始查询的时间
  • domainLookupEnd 表明DNS查询结束

很简单,作个减法,咱们就能拿到DNS查询的耗时。

// Measuring DNS lookup time
var pageNav = performance.getEntriesByType("navigation")[0];
var dnsTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;
要注意一点,这两个值可能都是 0,当咱们的资源是非同源的时候,假设多是用了第三方的CDN服务,且没有携带 Timing-Allow-Origin 的响应头。

创建链接

在与服务器创建链接以后,相关的资源才会发送到客户端。若是这个时候用了HTTPS协议,这个创建链接的过程就会多一步TLS握手。与此相关的3个指标以下:

  • connectStart 表示链接开始创建
  • secureConnectionStart 表示TLS握手开始
  • connectEnd 表示链接创建完成(同时也是TLS握手结束)

至于为何没有 secureConnectionEnd 这个属性,应该是TLS的握手是在创建链接的最后一步,与 connectEnd 是一个时间点。

若是用的不是HTTPS协议,则 secureConnectionStart0,因此咱们能够作一些兼容性的处理,以下代码:

// Quantifying total connection time
var pageNav = performance.getEntriesByType("navigation")[0];
var connectionTime = pageNav.connectEnd - pageNav.connectStart;
var tlsTime = 0; // <-- Assume 0 by default

// Did any TLS stuff happen?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

在DNS查询和创建链接完成后,真正的请求才开始了。

请求与响应

当咱们去思考究竟是什么影响了请求速度的时候,通常能够归类为如下两点:

  • 外在因素: 网络延迟或者带宽,这些都是开发者没法掌控的。
  • 内在因素:服务器和客户端的架构、资源大小等等。

和这部分相关性能指标是重中之重。Navigation和Resource Timing都有以下相关指标:

  • fetchStart 表示浏览器开始获取资源的时间,并不是是说从服务器获取,而是从检查缓存开始。
  • workerStart 表示从 [service worker]() 开始获取资源的时间,若是没有安装service worker,则是 0
  • requestStart 表示浏览器开始发起网络请求的时间
  • responseStart 表示服务器响应的第一个字节到达的时间
  • responseEnd 表示服务器响应的最后一个字节到达的时间,即下载完成

咱们能够用如下代码来获取资源下载的时间,以及缓存读取的时间

// Cache seek plus response time
var pageNav = performance.getEntriesByType("navigation")[0];
var fetchTime = pageNav.responseEnd - pageNav.fetchStart;

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

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

也能够去获取一些对咱们有帮助的组合时间,代码以下:

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

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

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

其余

以上,咱们已经获取了大部分重要的性能指标,但还有一些其余的指标也能够简单了解一下。

文档卸载

文档卸载发生在浏览器即将打开新的文档以前,通常而言,这不会出现什么大问题。但若是你绑定了 unload 事件,并在事件回调中执行了一些耗时的代码,你就须要去关注一下 unloadEventStartunloadEventEnd 这两个指标了。

unload 相关的指标只属于 Navigation Timing

跳转

通常状况下,跳转不是什么大问题,但若是频繁跳转,也会或多或少的影响页面的加载速度,看自身状况决定是否须要关注着几个指标 redirectStartredirectEnd

文档解析

文档加载以后,浏览器会解析文档。通常除非咱们的文档特别大,解析的耗时才会影响页面加载。Navigation Timing提供了相关指标 domInteractivedomContentLoadedEventStartdomContentLoadedEventEnddomComplete

文档解析相关的指标也只属于 Navigation Timing。

加载

当文档和资源都加载完了以后,浏览器会触发一个 load 事件,这时相关的回调函数会依次执行,咱们也能够去拿到加载时间的指标 loadEventStartloadEventEnd

以上两个指标也只属于 Navigation Timing

文档和资源的大小

文档和资源的大小毫无疑问是影响页面加载性能的关键因素。用API也可以拿到这些指标:

  • transferSize 表示资源传输总大小,包含header
  • encodedBodySize 表示压缩以后的body大小
  • decodedBodySize 表示解压以后的body大小

如下代码能够获取到一些其余信息:

// HTTP header size
var pageNav = performance.getEntriesByType("navigation")[0];
var headerSize = pageNav.transferSize - pageNav.encodedBodySize;

// Compression ratio
var compressionRatio = pageNav.decodedBodySize / pageNav.encodedBodySize;

其实资源和文档的大小都是开发者本身知道的,能够经过开发者工具看到,不必定要用API来获取这些信息。

在代码中实际应用

基本上上面对这些API都有了一个大体的了解,如今咱们能够在代码中去收集这些指标数据了。

其余获取性能条目的函数

上面咱们讲到一个 getEntriesByType 的函数能够获取指定类型的性能条目,还有另外两种:

getEntriesByName

getEntriesByName 能够经过名字来获取对应的条目。对 Navigation 和 Resource Timing 来讲,名字就是文档或资源的URL地址:

// Get timing data for an important hero image
var heroImageTime = performance.getEntriesByName("https://somesite.com/images/hero-image.jpg");

getEntries

getEntriesByTypegetEntriesByName 不同,getEntries 获取了全部的条目。

// Get timing data for all entries in the performance entry buffer
var allTheTimings = performance.getEntries();
这里咱们有一个概念没提到 initiatorType,有兴趣能够去 MDN 上查询相关资料

用 PerformanceObserver 来监听性能条目

上面咱们提到的三种函数都是一次性获取性能条目的,但这些都有如下两个问题:

  • 循环遍历性能条目的数组(可能很大),会阻塞主线程
  • 没法统计到新的请求或者新的指标。若是咱们用定时器来尝试解决这个问题,代价太大,甚至可能会引起渲染冲突,致使jank

PerformanceObserver 就是为此而诞生的。如下是相关代码:

// Instantiate the performance observer
var perfObserver = new PerformanceObserver(function(list, obj) {
  // Get all the resource entries collected so far
  // (You can also use getEntriesByType/getEntriesByName here)
  var entries = list.getEntries();

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

// Run the observer
perfObserver.observe({
  // Polls for Navigation and Resource Timing entries
  entryTypes: ["navigation", "resource"]
});

须要注意的是 PerformanceObserver 目前还没不适用于全部浏览器,须要作一些兼容处理:

// Should we even be doing anything with perf APIs?
if ("performance" in window) {
  // OK, yes. Check PerformanceObserver support
  if ("PerformanceObserver" in window) {
    // Observe ALL the performance entries!
  } else {
    // WOMP WOMP. Find another way. Or not.
  }
}

一些陷阱

看上去统计上面这些性能指标都很简单,但还有一些比较棘手的状况。

Cross-origins 和 Timing-Allow-Origin 的响应头

并不是全部的性能指标咱们都能获取到,若是没有携带一些响应头,某些指标可能就一直是 0,想要彻底掌握这部分,须要去标准文档细读。

持久链接会影响时序

当HTTP/1.1的请求带了 Connection: Keep-Alive 的响应头的时候,此链接会被复用。或者当咱们用的是HTTP/2的时候,一个链接会被全部同源资源复用。这些都会影响时间统计,不过咱们不用太刻意去检查这些,稍微留个心就行了。

不是全部浏览器都支持这些API

对Web开发者而言,浏览器兼容性是没法避免的问题。并且 getEntriesByType 这个API函数,若是获取一个不支持的类型的性能条目,浏览器并不会报错,而是返回空数组,如如下代码:

// This returns stuff!
performance.getEntriesByType("resource");

// Not so much. :\
performance.getEntriesByType("navigation");

为此,咱们能够稍做兼容:

if (performance.getEntriesByType("navigation").length > 0) {
  // Yay, we have Navigation Timing stuff!
}

并不是全部浏览器都支持这些API,用的时候尽可能作一些检测,避免产生一些错误的统计。

收集数据

咱们已经知道了如何使用这些API获取性能指标,但这些数据咱们应该放在哪里?

使用navigator.sendBeacon

navigator.sendBeacon 是一种非阻塞的请求方式,不用等待服务器响应,只是单方面的数据发送,是收集RUM数据的一个最佳方案,即便页面关闭,浏览器依然会将这些请求发送完成。

// Caution: If you have a _lot_ of performance entries, don't send _everything_ via getEntries. This is just an example.
let rumData = JSON.stringify(performance.getEntries()));

// Check for sendBeacon support:
if ('sendBeacon' in navigator) {
  // Beacon the requested
  if (navigator.sendBeacon('/analytics', rumData)) {
    // sendBeacon worked! We're good!
  } else {
    // sendBeacon failed! Use XHR or fetch instead
  }
} else {
  // sendBeacon not available! Use XHR or fetch instead
}

服务端要获取这些数据,能够从post表单中获取,或者从get的参数中获取。

navigator.sendBeacon 调用的时候,只是往队列里面插入了一个,等待浏览器资源空闲,会将请求发送出去。若是资源过大,浏览器也可能会拒绝发送。

总结

若是你对这些还不够自信,千万不要直接就应用在项目代码中,建议详细阅读相关标准文档以后,再尝试应用在项目中。有了这些性能指标数据,咱们能够随时修复一些发现的问题。

另外,你也不用把全部指标都存到服务器,选一些本身以为有用的就好。

本文档只是一个引导性质的,并不能彻底表明这些API的全部使用方式,建议仍是阅读如下相关标准文档(文中连接)。

有了这些API,你就能更加了解真是用户的使用场景。

参考

https://developers.google.com...

相关文章
相关标签/搜索