web性能之资源加载时间分析【Resource Timing】【原创】

7,068 阅读13分钟

简介

在前面一章我们学习了 Web Performance Timing Api 可以用来分析web应用的资源加载、手动卡点的性能(时间),那么我们现在就来看看怎么使用Resource Timing 来分析我们的静态资源的加载情况。

案例:

在平时开发的时候,我们常会遇到这样的问题反馈;

客户反馈:朋友我怎么感觉我们这个应用怎么初始化加载就这么慢呢?到底是什么原因时快时慢的?

开发:我用我的(手机|电脑)测试了没有问题啊很快啊,而且我也用谷歌调试工具看了渲染时间确实很快啊。

这个时候对于我们开发来说,我们能想到的方式就是:

  • 先确认是否有脚本加载阻塞
  • 确认是否有资源加载过慢(本机,自己的网络)
  • 对于用户说的慢,其实我们自己也不知道是说的什么慢,然后就各种网上搜一波,优化一波,到底解没解决问题自己是测试不出来的(不知道有没有人跟我一样的感觉就是:开发的设备运行都没有什么问题,到了其他设备上就开始出问题了)。

其实遇到这个问题的最根本的原因是我们无法用 **数据来证明 **到底是哪里慢,什么情况慢,并且我们也无法提前预知到这些问题,总不能让客户把他的设备(手机|电脑)邮寄给你,然后你开始各种调试吧。

其实上面的这个案例牵扯很多的问题:

  • 不知道用户当时的实际网络情况 ☆☆☆☆
  • 不知道我们的代码在用户的设备运行情况 ☆☆☆ (本章不讲)
  • 不知道到底是我们的接口慢还是我们的代码由于运算的数据量过大导致慢 ☆☆☆☆(本章不讲)
  • 还是说代码在运行中由于个别的数据没有做数据验证(判空|类型不匹配...)导致页面报错(本章不讲)

可能还有其他的因素的影响;

对于我们来说我们应该优先处理可控、可预知的性能影响因素;

本地调试分析资源加载问题

分析前代码示例:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>demo</title>
  <link href="https://cdn.bootcss.com/animate.css/3.7.2/animate.css" rel="stylesheet">
  <link href="https://cdn.bootcss.com/video.js/7.7.6/alt/video-js-cdn.css" rel="stylesheet">
</head>
<body>
<img src="https://www.baidu.com/img/bd_logo1.png">
<img src="https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1585755631&di=5e0530559acfe3bc031c33c5c6fe38d6&src=http://a3.att.hudong.com/14/75/01300000164186121366756803686.jpg">
</body>
<script src="https://cdn.bootcss.com/video.js/7.7.6/alt/video.core.js"></script>
</html>

以上这个代码里面我们可以看到,我们分别引入了:

  • animate.css
  • video.js
  • video.css
  • 百度的logo
  • 还有一张桌面壁纸

我们先本地运行这个页面,然后看控制台,他们的耗时分别是多少,下图是在清除本地缓存后,模拟用户在200M光纤网下的页面打开速度

从上面的数据我们能在自己的本机分析出来,我们用时最久的资源是video.js这个文件,虽然我用的是CDN,但是其实感觉还是有点时间过长,这里我们先不考虑脚本加载阻塞的情况,我们只关注资源的加载时间分析问题上。

从这个图片中我们可以发现,有两个js文件的链接时间使用较长但是下载很快。

进过上面的一波本地调试器的分析过后,我们貌似找到了一些问题,但是不要忘了这只是在我们自己的设备上调试的结果,并且在网络很好的情况(200M光纤网)下。

但是对于用户的设备打开网页的真实情况我们并不清楚,所以我们需要抓取到这些数据(越多越好),并且做分析。

利用Resource Timing 抓取资源加载过程信息

我们先使用 performance.getEntriesByType('resource')方法获取到浏览器运行开始至调用该方法时的所有资源加载情况的数据。

我们在控制台输入

performance.getEntriesByType('resource')

我们首先清除缓存,然后从新运行代码,我们来看下本次的运行结果,这次的运行结果很有趣:

第一我们可以看到,同样的设备,同样的环境,但是我们的时间结果差距会这么大,居然还有3S以上的加载时间,这直接导致了我们的DOM渲染时间被拉长到了3S+。

如果这个情况不是出现在我们自己调试的时候,那么我们永远不可能发现这个问题,虽然我用的是第三方的CDN,但是还有是不稳定的时候。

这个时候我们来看我们在onload的时候我们调用的resource timing 信息返回是一个数组,这个数组返回的是我们这次从打开网页一开始到DOM渲染完成的过程里加载过得所有资源信息。

然后我们随意点开一个资源看看里面有什么信息是我们可以获取到的:

从上图中我们可以获取到什么多的信息,我们现在看看这些属性主要都是做什么用的:

PerformanceResourceTiming

属性 描述
entryType EntryType的类型resource
name resources URL
startTime 在资源提取开始的时间
duration 整个流程消耗的时间=responseEnd-startTime
initiatorType 发起资源请求的类型
nextHopProtocol 获取资源的网络协议的字符串
workerStart 如果Service Worker线程已在运行,则在调用FetchEvent之前立即返回DOMHighResTimeStamp,如果尚未运行,则在启动Service Worker线程之前立即返回DOMHighResTimeStamp。 如果资源未被Service Worker拦截,则该属性将始终返回0
redirectStart 初始重定向的开始获取时间
redirectEnd 紧接在收到最后一次重定向响应的最后一个字节后
fetchStart 拉取资源开始时间,紧接在浏览器开始获取资源之前
domainLookupStart 紧接在浏览器启动资源的域名查找之前
domainLookupEnd 表示浏览器完成资源的域名查找后的时间
connectStart 开始TCP连接:紧接在浏览器检索资源,开始建立与服务器的连接之前
connectEnd 结束TCP连接:紧接在浏览器完成与服务器的连接以检索资源之后
secureConnectStart 开始SSL连接:紧接在浏览器启动握手过程之前,以保护当前连接
requestStart 紧接在浏览器开始从服务器请求资源之前
responseStart 紧接在浏览器收到服务器响应的第一个字节后
responseEnd 紧接在浏览器收到资源的最后一个字节之后或紧接在传输连接关闭之前,以先到者为准
secureConnectionStart SSL / 初始连接时间
transferSize 表示获取资源的大小(以八位字节为单位)的数字。 包括响应头字段和响应payload body的大小。
encodedBodySize 在删除任何应用的内容编码之前,从payload body的提取(HTTP或高速缓存)接收的大小(以八位字节为单位)的number
decodedBodySize 在删除任何应用的内容编码之后,从消息正文( message body )的提取(HTTP或缓存)接收的大小(以八位字节为单位)的number
serverTiming 包含服务器时序度量( timing metrics )的PerformanceServerTiming 条目数组,可用于服务器传数据到前端
PerformanceResourceTiming.initiatorType

已知可获取类型如下:

类型 描述
css css资源类型
img 图片请求类型
scrpit scrpit脚本请求类型
xmlhttprequest 接口请求类型
link link请求类型

上述的表格中列出了 resource 类型时的所有属性及含义:

我们一般来直接把这个数据结构体转发给后台进行分析就可以了

注意:上面的所有时间都是相对时间,全部都是相对这个程序一开始运行的叠加时间,不是系统的时间戳

那么我们有集合上面的数据来进行简单的分析,得到的结果如下:

我们这个名叫https://cdn.bootcss.com/video.js/7.7.6/alt/video-js-cdn.css的这个资源文件,从创建资源initiatorType开始到DNS解析结束domainLookupEnd的这段时间都很顺滑。

但是到了建立链接这一步(SSL连接)secureConnectionStart这一步就开始卡了,并且卡了3S+,才完成了安全连接的建立,从这时间来看是很不正常的,随后后面的下载、解析操作都是相当的快。

那么到这里我们其实就发现了,我们在开发时可能复现不了的问题,(本次是我们在开发时用调试器有幸看到了),要是用户在使用的时候,对于我们来说我们是无知的,也没有做数据记录的,所以为什么有时候客户说慢但是我们却复现不出来的原因(当然还有可能是网络质量问题,后面我会讲怎么获取用户当前网络质量)。

分析数据

这里我们可以直接用js分析一次我们抓取到的数据:

代码修改如下

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>demo</title>
  <link href="https://cdn.bootcss.com/animate.css/3.7.2/animate.css" rel="stylesheet">
  <link href="https://cdn.bootcss.com/video.js/7.7.6/alt/video-js-cdn.css" rel="stylesheet">
  <style>
    img {
      width: 50px;
    }

    .analysisResult {
      margin-top: 50px;
    }

    table {
      margin-top: 8px;
    }

    td {
      min-width: 70px;
      padding: 0 6px;
    }

    .status {

    }

    .status-item {

    }

    .color {
      display: inline-block;
      height: 10px;
      width: 10px;
    }

    .success {
      color: green;
    }

    .success_back {
      background: green;
    }

    .warning {
      color: #E6A23C;
    }

    .warning_back {
      background: #E6A23C;
    }

    .error {
      color: red;
    }

    .error_back {
      background: red;
    }
  </style>
</head>
<body>
<img src="https://www.baidu.com/img/bd_logo1.png">
<img
    src="https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1585755631&di=5e0530559acfe3bc031c33c5c6fe38d6&src=http://a3.att.hudong.com/14/75/01300000164186121366756803686.jpg">
<div class="analysisResult">
  <div class="status">
    <span class="status-item">
      <span class="color success_back"></span>
      <span class="text"><=50ms</span>
    </span>

    <span class="status-item">
      <span class="color warning_back"></span>
      <span class="text"><=100ms</span>
    </span>

    <span class="status-item">
      <span class="color error_back"></span>
      <span class="text">>100ms</span>
    </span>
  </div>
  <table border="none">
    <thead>
    <tr>
      <th>文件名称</th>
      <th>标签类型</th>
      <th>文件类型</th>
      <th>解析时间</th>
      <th>连接时间</th>
      <th>响应时间</th>
      <th>文件大小</th>
      <th>开始时间</th>
      <th>结束时间</th>
      <th>消耗时间</th>
    </tr>
    </thead>
    <tbody id="tableBody">
    </tbody>
  </table>
</div>
</body>
<script src="https://cdn.bootcss.com/video.js/7.7.6/alt/video.core.js"></script>
<script>
  window.onload = function () {
    let resourceLoadTiming = performance.getEntriesByType('resource');
    let tableBody = document.querySelectorAll('#tableBody')[0];
    console.log(resourceLoadTiming);
    resourceLoadTiming.map(({startTime, duration, name, transferSize, responseStart, responseEnd, secureConnectionStart, requestStart, domainLookupStart, domainLookupEnd, initiatorType}) => {
      if (initiatorType === 'css') return;
      const tr = document.createElement('tr');
      let trClass = '';
      let fileName = name.replace(/(^.*\/)(.*)(\..*$)/, '$2$3');
      let fileType = name.replace(/(^.*)(\..*$)/, '$2');
      if (duration <= 50) {
        trClass = 'success';
      } else if (duration <= 100) {
        trClass = 'warning';
      } else {
        trClass = 'error';
      }
      tr.setAttribute('class', trClass);
      tr.innerHTML = `
        <td>${fileName}</td>
        <td>${initiatorType}</td>
        <td>${fileType}</td>
        <td>${(domainLookupEnd - domainLookupStart).toFixed(2)}ms</td>
        <td>${(requestStart - secureConnectionStart).toFixed(2)}ms</td>
        <td>${(responseEnd - responseStart).toFixed(2)}ms</td>
        <td>${(transferSize / 1000).toFixed(2)}kb</td>
        <td>${startTime.toFixed(2)}ms</td>
        <td>${responseEnd.toFixed(2)}ms</td>
        <td>${duration.toFixed(2)}ms</td>
      `;
      tableBody.appendChild(tr);
    });
  };
</script>
</html>

我们在清除缓存后,在运行一次代码后就会在页面上看到我们的分析的数据结果如下:

上面中因为我们设置了各种等级的阈值,所以可以看到没有一个资源是在50ms内加载完成的,当然如果是实际的分析中,我们还需要加上一个比例不是所有的数据都能在50ms内返回,比如我们下载的是10M的文件这个时候的合理阈值就不会是50ms 了。

下面我们再来看看,我们在控制台看到的数据是否和我们用js分析出来的数据是否一致:

从上图中我们可以看到其实我们在控制台的时间和我们分析出来的时间是一致的。

其实如果再想确认数据的准确性的话,就需要用缓存把我们每次清除缓存后重新加载的数据缓存起来,然后做一个求平均值处理,比如执行100次看平均数据,数据越多越接近真实情况

如果想看实际的运行效果**请搓这里**

总结

通过上述的一顿操作,我们发现其实,对于资源加载这块我们是可以通过Resource Timing来分析得到的,所以一个稳定高性能的应用是需要做数据收集分析,并且还需要做预计功能的,这样才能保证能够提前预知一些问题,方便我们开发人员进行维护和调优。

通过上面获取的数据我们可分析出以下数据:

  • 资源请求的DNS的解析时间
  • TCP连接所消耗的时间
  • SSL连接所消耗的时间
  • 请求时间(服务器处理时间)
  • 响应时间(下载时间)
  • 传输的文件大小
  • 编码前的文件大小
  • 解码后的文件大小

未来规划

关于资源分析这块我自己也在空余时间开发一个分析资源加载的小插件WebResourceTiming

预计会在下周发布V1.0主要功能如下:

  • 可以快速获取到格式化后的数据(上面描述的各类时间都帮你计算好了)
  • 支持订阅者/发布者模式,支持实时监控获取最新资源数据
  • 还会有一个基于WebResourceTiming模块开发的资源分析报告小插件WebResourceTimingReport到时都会发布到npm,无任何第三方依赖。
    • WebResourceTimingReport支持配置化阈值和钩子函数,自定义结果,如果不设置则按默认配置分析,生成报告
    • 支持不生成报告,只获取渲染报告的数据,方便前端传输回后台进行数据搜集

后面还会陆续发布剩余的几大类的API的使用介绍文章,和应用场景及相关数据分析小插件,及相关插件的使用文档

  • WebRrameTiming 如何分析框架渲染性能信息,并附带小插件
  • WebNavigationTiming 如何分析导航过程信息,并附带小插件
  • WebMarkTiming 如何利用手动打点分析我们的代码性能及追踪用户行为数据,并附带小插件
  • WebPerformanceAnalysis 如何上面的小插件完成整个web网页的性能分析,及性能报表生成及相关的优化建议,负载组件库

2020年4月3日

JBoss