关于web:页面加载性能之感知真实世界

10次阅读

共计 7847 个字符,预计需要花费 20 分钟才能阅读完成。

尽管咱们能够通过开发者工具以及 lighthouse 等工具来查看网站的加载状况,并按之前咱们说的那些计划做好了优化,但真正用户关上是否真的如预期个别快,咱们不得而知。始终以来咱们都以实验室数据为测试的根据,这些不能代表现场数据,即实在用户的体验。

RUM(Real User Monitoring)因而而诞生。RUM 依赖于浏览器提供的 API 来收集实在用户的性能数据,次要蕴含 2 个规范文档,Navigation Timing API 和 Resource Timing API,这两个 API 都是基于 High Resolution Time 的标准定制的。

本文档将疏导你去意识这些 API 提供的数据,更好的把握 RUM。

浏览器中的网络申请

Navigation 和 Resource Timing 之间有局部交加,但两者收集的数据指标还是不一样的。

  • 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…

正文完
 0