Web性能优化-加载优化(二)

1,517 阅读6分钟

本文主要叙述通过导航(navigation)和资源(resource)评估加载性能。

通过导航和资源评估加载性能

可以通过performance提供的api获取导航和资源计时

导航和资源计时之间存在明显的重复覆盖,但它们各自收集不同方面的指标:

  • 导航(navigation)计时收集HTML文档的性能指标。
  • 资源(resource)计时收集文档相关资源的性能指标。东西像样式表,脚本,图片,等等。

(导航)PerformanceNavigationTiming接口定义的计时演示如下:

(资源)PerformanceResourceTiming接口定义的计时演示如下:

导航和资源计时(以及其他相关API)将性能数据存储在缓冲区中。它们将页面和资源的性能指标存储到JavaScript可访问的列表中。这些方法存在于window.performance名称空间中。我们可以使用getEntriesByType进行访问。

// 获取导航计时项:
performance.getEntriesByType("navigation");

// 获取资源计时项:
performance.getEntriesByType("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中的瀑布流显示了其信息

我们也可以在某个资源的Timing面板中查看各阶段耗时情况

接下来分阶段描述各阶段情况:

DNS查询

当用户请求URL时,将查询域名系统(DNS)以将域转换为IP地址。由于许多因素(尤其是DNS缓存)影响,此过程可能会花费大量时间。导航和资源计时都公开了两个与DNS相关的指标:

  • domainLookupStart DNS查找开始时标记。
  • domainLookupEnd DNS查找结束时标记。

衡量dns查询时间

var pageNav = performance.getEntriesByType("navigation")[0];
var dnsTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

但是注意:跨域资源是否可访问该数据还受到http请求头影响,响应头Timing-Allow-Origin用于指定特定站点,以允许其访问Resource Timing API提供的相关信息,否则这些信息会由于跨源限制将被设置为零。

连接协商

与服务器建立连接后,在客户端请求服务器和服务器将资源发送给客户端之前,客户端和服务器会进行处理,从而发生延迟。如果使用HTTPS(这越来越普遍),则此过程还包括TLS协商时间。连接阶段包含三个指标:

  • connectStart 客户端打开与服务器的连接时标记。
  • secureConnectionStart 在客户端开始TLS协商时标记。
  • connectEnd 在连接协商结束时标记(包括TLS时间)。

这些几乎和DNS指标一样,但是secureConnectionStart有些不同,为什么会没有"secureConnectionEnd",因为TLS结束和connectEnd是同时的。非https下secureConnectionStart为0。

// 查询总的连接时间
var pageNav = performance.getEntriesByType("navigation")[0];
var connectionTime = pageNav.connectEnd - pageNav.connectStart;

// 查询tls连接时间
var tlsTime = 0;
if (pageNav.secureConnectionStart > 0) {
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

请求和响应

考虑影响页面速度的因素时,我们应该考虑两个因素:

  • 外在因素:这是诸如连接延迟和带宽之类的东西。他们(大部分)是我们无法控制的。
  • 内在因素:这些是我们可以更好地控制的事情,例如服务器和客户端架构以及资源大小。

两种类型的因素都会影响请求和响应速度。导航和资源计时都使用以下方式衡量请求和响应:

  • fetchStart 当浏览器开始获取资源时标记。这与请求不同,因为它不标记浏览器何时发出网络请求,而是标记它何时开始检查缓存(例如HTTP和Service-worker缓存)从而确定是否需要网络请求。
  • workerStart 在fetch事件处理程序从开始service-worker获取请求时标记。如果当前页面未安装service-worker,则此标记为0。
  • requestStart 当浏览器发出网络请求时。
  • responseStart 当响应的第一个字节到达时。
  • responseEnd 当响应的最后一个字节到达时。

可以使用这些指标来衡量加载性能的许多方面。例如,可以衡量资源下载时间的同时计算缓存查找时间:

// 缓存查找加上响应时间
var pageNav = performance.getEntriesByType("navigation")[0];
var fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// service-worker响应时间
var workerTime = 0;

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

还可以测量其他的时间:

// 单独请求的时间
var requestTime = pageNav.responseStart - pageNav.requestStart;

// 单独响应的时间
var responseTime = pageNav.responseEnd - pageNav.responseStart;

// 请求开始到响应结束的时间
var requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

页面卸载

卸载是指浏览器在加载新页面之前进行一些整理工作。大多数情况下不需要关注。但是如果有一些代码在unload事件中处理,则需要考虑unloadEventStartunloadEventEnd

注意:卸载计时是"navigation timing"所独有的。

重定向

重定向会增加请求的延迟,因此有时也需要对重定向进行测量。可以使用API都提供 redirectStart 和 redirectEnd 指标。

页面处理

可以通过domInteractive(可交互), domContentLoadedEventStart(dom加载开始), domContentLoadedEventEnd(dom加载完成),和 domComplete(文档准备就绪)来进行计算。

如图红色区域所示:

载入中

当文档及其资源完全完成加载后,浏览器将触发一个load event。loadEventStart 并且 loadEventEnd 可以测量加载时间。

var pageNav = performance.getEntriesByType("navigation")[0];
var loadTime = pageNav.loadEventEnd - pageNav.loadEventStart;

文件和资源大小

文档或资源的大小无疑对加载性能有影响。我们可以通过以下api度量:

  • transferSize是资源的总大小,包括HTTP标头。
  • encodedBodySize是资源的压缩大小,不包括 HTTP标头。
  • decodedBodySize是资源的解压缩大小,不包括HTTP标头。

计算响应的http头部大小和压缩率:

// HTTP 头部大小
var pageNav = performance.getEntriesByType("navigation")[0];
var headerSize = pageNav.transferSize - pageNav.encodedBodySize;

// 压缩率ression ratio
var compressionRatio = pageNav.decodedBodySize / pageNav.encodedBodySize;

手动获取时间

  • getEntriesByName

    getEntriesByName 通过名称获取性能列表。对于导航和资源衡量来说,需要传入导航或资源的url:

    var imageTime = performance.getEntriesByName("https://somesite.com/images/some-image.jpg");
    
  • getEntries

    与getEntriesByName和getEntriesByType不同,getEntries可以获取到所有的性能列表。

    var allTheTimings = performance.getEntries();
    

使用PerformanceObserver监听

例如getEntriesByType这种获取方式,需要循环来使用他们,可能会造成如下问题:

  • 循环大型数组,会阻塞主线程
  • 只能获取循环运行时刻的性能列表,需要计时器定期轮询,会导致与渲染器竞争。

PerformanceObserver提供了类似MutationIntersection Observer的观察者模式,可以提供一个回调。

// 初始化Observer
var perfObserver = new PerformanceObserver(function(list, obj) {
  // 可以获取所有的列表
  // 也可以使用getEntriesByType/getEntriesByName
  var entries = list.getEntries();

  for (var i = 0; i < entries.length; i++) {
    // 做些事情!
  }
});

// 开始观测
perfObserver.observe({
  entryTypes: ["navigation", "resource"]
});

注意:IE下暂不支持PerformanceObserver,使用前先检查此功能:

if ("performance" in window) {
  if ("PerformanceObserver" in window) {
  } else {
  }
}

注意事项

  • 持久连接会影响计时 使用Connection:Keep-Alive或http2时,使用单个连接传送所有资源可能会对计时产生影响。
  • 浏览器兼容性 部分浏览器支持不完善,使用前需要检查

上报

可以使用navigator.sendBeacon进行数据上报

let rumData = JSON.stringify(performance.getEntries()));

if ('sendBeacon' in navigator) {
  if (navigator.sendBeacon('/analytics', rumData)) {
  } else {
    // 使用xhr或者fetch代替
  }
} else {
  // sendBeacon 不可用 使用xhr或者fetch代替
}