Web 前端性能分析(一)

参考连接

  1. 初探 performance – 监控网页与程序性能
  2. 使用简洁的 Navigation Timing API 测试网页加载速度
  3. 前端性能统计
  4. 前端性能——监控起步
  5. 使用性能API快速分析web前端性能
  6. Page Visibility

经过以上几篇文章,能够对前端性能相关的概念和 API 有一个总体的认识。javascript

简要说明

前段时间和同事一块儿对网页性能监控方面的知识作了些探讨和实践,指望能够对用户的网络状况、程序的性能情况等作个统计分析,从而对程序进行有针对性的优化。为此咱们作了个简单的试验项目,主要对 页面加载ajax请求 两个方面进行了分析。(本文的方案主要是出于技术探讨的目的,只是一个 Demo,而非完整的性能监控方案)css

Web前端统计.PNG-40.3kB

这个图是最初的方案图,咱们初级版本的程序设计基本上就是按照图上这个思路来的。html

咱们的实现思路是,在页面初始化完成后,将本次页面加载的信息用户上次页面操做过程当中发出的ajax请求信息上报给服务器,由服务端进行进一步统计分析。前端

页面加载信息,主要指css样式表、js脚本和图片等外部资源加载用时和初始化完成的时间(所有完成用时)。
用户上次页面操做过程当中发出的ajax请求,主要是指用户上一次在这个页面上进行的查询、自定义设置等操做过程当中,触发的ajax请求相关的信息,好比方法名称、服务器处理时间、客户端下载时间等。html5

为何是用户上次操做的ajax相关信息?
主要是出于减小请求的目的,以免监控程序自己对程序主体性能的影响,所以不会将每一个请求的信息都实时的上报服务器,而是先存储在客户端。咱们会将用户在这个页面进行的各类操做触发的异步请求信息,以必定格式存储在客户端 localstorage,当用户再次打开这个页面的时候,咱们会从 localstorage 中取出存储的ajax信息,将其上报服务器,而后清空 localstorage 中这些旧的数据,以便从新进行记录。java

所以,用户在打开这个页面时,咱们上报的是用户上次的使用信息。(若是有用户只打开过一次这个页面,后面就再没使用过,那么这是一个低频使用客户,不在咱们统计范围内。)jquery

而用户的页面加载信息,每次用户打开页面时,咱们都会将其上传至服务器,不须要在客户端进行存储。web

服务端收到前端上报的数据后,会进行相应的分析处理,这里不对这部分进行说明。ajax

相关知识

1、影响网页性能的因素

  1. HTML 的解析和渲染(参见文档 《浏览器解析渲染HTML页面的过程》
  2. 服务端处理的速度(负载均衡,缓存策略)
  3. 客户端带宽(网络情况)

咱们要对网页的性能进行统计分析,首先应当肯定哪些因素会对网页的性能带来影响。通常来讲,前端HTML文档的结构是否合理,外部资源是否进行了压缩合并,静态内容是否使用了CDN加速,服务端是否配置了负载均衡,是否采起了缓存策略,以及客户端带宽情况等,都会对网页的性能形成影响。segmentfault

2、浏览器解析渲染HTML页面的过程

参考资料: 浏览器的工做原理

上面这篇文章会帮助咱们了解浏览器解析和渲染HTML文档的过程。具体的能够参见另外一篇文档: 《浏览器解析渲染HTML页面的过程》

这里对如下几点进行着重说明:

  1. HTML 文档的解析和渲染是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它没必要等到整个 HTML 文档解析完毕,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其他内容的同时,呈现引擎会将部份内容解析并显示出来。
  2. 浏览器的预解析机制。
  3. HTML 文档的解析和渲染过程当中,外部样式表和脚本顺序执行、并发加载

JS 脚本会阻塞 HTML 文档的解析,包括 DOM 树的构建和渲染树的构建;CSS 样式表会阻塞渲染树的构建,但 DOM 树依然继续构建(除非遇到 script 标签且 css 文件此时仍未加载完成),但不会渲染绘制到页面上。
在 HTML 文档的解析过程当中,解析器遇到 <script> 标记时会当即解析并执行脚本,HTML 文档的解析将被阻塞,直到脚本执行完毕。若是脚本是外部的,那么解析过程会中止,直到从网络抓取资源并解析和执行完成后,再继续解析后续内容。
但不管是哪一种状况致使的阻塞,该加载的外部资源仍是会加载,例如外部脚本、样式表和图片。HTML 文档的解析可能会被阻塞,但外部资源的加载不会被阻塞。

3、浏览器并发链接数

Chrome: Browser only allows six TCP connections per origin on HTTP 1.

Chrome 浏览器的并发链接数为 6 个,超过限制数目的请求会被阻塞。

参见《浏览器解析渲染HTML页面的过程》的 “CSS 和 JS 的处理顺序和阻塞分析”一节。

4、Performance API

可以实现对网页性能的监控,主要是依靠 Performance API。

  1. 《JavaScript 标准参考教程(alpha)》
  2. MDN文档

重点查看如下方法:

  1. Performance.timing
  2. Performance.getEntries()
  3. Performance.getEntriesByType()
  4. Performance.now()

尤为是第一项,能够在控制台输出查看一下。

5、localStorage

  1. Web Storage API
  2. calculating-usage-of-localstorage-space

localStorage 的基本概念和使用方法能够参见上面的连接,包括测试本地存储是否已被填充、从存储中获取值、在存储中设置值、删除数据记录、浏览器兼容性、经过 StorageEvent 响应存储的变化等。

localStorage 的大小限制
浏览器对于 localStorage 存储数据的大小有限制,通常为 5M/域,所以开发时应该注意控制存数数据的大小,并按期清除过时和无用的数据。

当 localStorage 存储超限的时候,会报 Uncaught QuotaExceededError 错误。

// 当存储数据大小超过限制时,会报如下错误:
// `YourStorageKey` 指报错时存放数据的键值
Uncaught QuotaExceededError: Failed to set the 'YourStorageKey' property on 'Storage': Setting the value of 'YourStorageKey' exceeded the quota.

咱们可使用 try-catch 对数据存储操做进行包裹,当捕获数据超限的错误时,咱们能够先清除旧数据再进行存储。

// 存储 xhr 信息到客户端 localStorage 中
wp.setItemToLocalStorage = function (xhr) {
    var arrayObjectLocal = this.getItemFromLocalStorage();
    if (arrayObjectLocal && Array.isArray(arrayObjectLocal)) {
        arrayObjectLocal.push(xhr);
        try {
            localStorage.setItem('webperformance', JSON.stringify(arrayObjectLocal));
        } catch (e) {
            if (e.name == 'QuotaExceededError') {
            // 若是 localStorage 超限, 移除咱们设置的数据, 再也不存储
            localStorage.removeItem('webperformance');
            }
        }
    }
};

数据格式
localStorage 只能存储字符串类型的数据,不可以直接存储数组或对象。但咱们能够经过 JSON.stringify()JSON.parse() 实现对数组和对象数据类型的存取.

localStorage.setItem('webperformance', JSON.stringify(arrayObjectLocal));
var arrayObjectLocal = JSON.parse(localStorage.getItem('webperformance')) || [];

网页性能指标

1、页面性能指标

  1. 白屏时间
    读取页面首字节时间(ttfb - Time To First Byte),能够理解为用户拿到页面资源占用的时间。
    浏览器对html文档的解析和渲染是一个渐进的过程,通常在拿到首字节以后便会有内容绘制在页面上,正常网络状态下基本上白屏时间很短。
  2. 资源加载
    浏览器在接收到服务器返回的 html 文档数据以后,会起一系列的线程去请求文档解析中遇到的各类资源,js脚本、CSS样式表、图片,以及发起异步请求。咱们这里的资源认为是 js/css/图片,后面统计资源加载状况时,会统计这些资源的文件大小、文件数量、总的加载用时。ajax异步请求咱们会另外进行统计。
  3. 用户可操做时间
    在查阅相关资料时,会看到用户等待页面时间、用户可操做时间等概念,不一样资料和文章的定义也不一样,这里咱们认为用户可操做时间就是用户能够进行页面操做的时间,此时 html 文档解析完成(domContentLoadedEventEnd)。另外一种用户等待页面的时间,通常是按照页面加载完成的时间来统计(loadEventEnd)。但在咱们此次的前端性能监控方案中,并不将其做为主要的监控指标。
  4. 首屏渲染时间
    首屏时间的统计比较复杂,由于涉及图片资源的下载及异步请求等因素。有些资料统计中不计算图片的下载时间,但咱们认为既然是首屏的展现,应当包括图片加载的完成。判断首屏图片加载完成的方法,这里再也不详述,能够查阅相关文章。咱们此次的前端性能分析方案中,并无涉及到图片,而是关注页面初始化过程当中的异步请求。

2、ajax 请求性能指标

  1. 服务器处理时间
  2. 客户端下载时间
  3. 接口名称
  4. 下载速度
  5. 页面路径及id
  6. 传输大小

代码说明

1、模块构成

web-performance.js

  1. 兼容 CommonJS AMD CMD 及 原生JS
  2. 无第三方依赖(好比jquery)
  3. 主要提供如下方法:

    var wp = {
        generateGUID, // 生成当前页面惟一 id
        showInfoOnPage, // 在当前页面显示相关信息
        recordAjaxInfo, // 记录页面初始化完成前的 ajax 信息, 或者打印初始化完成后的 ajax 信息到页面
        sendPerformanceInfoToServer, // 上报服务器
        setItemToLocalStorage, // 存储 xhr 信息到客户端 localStorage 中
        getItemFromLocalStorage, // 获取客户端存储的 xhr 信息, 返回数组形式
        getDesignatedXHRByRequestId, // 经过 requestId 获取特定 xhr 信息
        getPageInitCompletedInfo, // 获取页面初始化完成的耗时信息
        // ......
    };

2、与业务代码的结合

咱们实现了性能监控模块 web-performance.js,那么怎么在应用中使用?
若是只是实现对页面加载信息的分析,那么在业务代码中只须要引入这个模块,而后在业务代码中页面初始化完成时调用模块的方法便可。可是,若是要实现对每个ajax请求的统计分析,就须要配合封装 ajax 文件。

  1. 封装的 ajax 文件中引入性能监控模块

    var WebPerformance = require('./web-performance'); // 网页性能监控模块
    var requestIdentifier = {};
  2. 每一个请求生成惟一标识

    triggerService: function (serviceName, input, success, error, ajaxParams) {
        var request = ajaxRequest.ajax.buildServiceRequest(serviceName, input, success, error, ajaxParams);
    
        // 生成这次 ajax 请求惟一标识
        var requestId = requestIdentifier[serviceName] = WebPerformance.generateGUID();
        request.url = URL + requestId;
        return ajaxRequest.ajax(request, serviceName, requestId);
      }
    
    ajaxRequest.ajax = function (userOptions, serviceName, requestId) {
        userOptions = userOptions || {};
        var options = $.extend({}, ajaxRequest.ajax.defaultOpts, userOptions);
        options.success = undefined;
        options.error = undefined;
    
        return $.Deferred(function ($dfd) {
          $.ajax(options)
            .done(function (result, textStatus, jqXHR) {
              // 每次请求都会有惟一id,请求返回时比对id是否变化 
              if (requestId === requestIdentifier[serviceName]) {
                ajaxRequest.ajax.handleResponse(result, $dfd, jqXHR, userOptions, serviceName, requestId);
              }
            })
            .fail(function (jqXHR, textStatus, errorThrown) {
              if (requestId === requestIdentifier[serviceName]) {
                //jqXHR.status
                $dfd.reject.apply(this, arguments);
                userOptions.error.apply(this, arguments);
              }
            });
        });
      };
  3. 在成功的回调中对xhr信息进行客户端存储等操做

    try {
        // 将这次请求的信息存储到客户端的 localStorage
        var headers = jqXHR.getAllResponseHeaders();
        var xhr = WebPerformance.getDesignatedXHRByRequestId(requestId, serviceName, headers);
        WebPerformance.setItemToLocalStorage(xhr);
        WebPerformance.recordAjaxInfo(xhr); // 要在成功的回调以前调用
    } catch (e) {throw e}

具体实现逻辑参见源码 - Web 前端性能分析(二)

3、接口调用

web-performance.js 模块自己简单封装了原生 ajax ,后台提供了上报服务器的接口。这里的请求不能使用业务代码中封装的 ajax 文件,由于不能将上报性能信息的请求也统计在内。

// 页面信息上报参数模型
{
  name: Page,
  data: {
    "pageLoad": 991,
    "ttfb": 46,
    "domReady": 985,
    "onload": 1,
    "tcpConnect": 0,
    "startTime": 1531209356934,
    "pageInitCompleted": 1676.6999999963446,
    "pageUrl": "/xxx/index.html",
    "pageId": "df393fc4-390b-4661-b4ea-002237958051"
  }
}

// ajax请求上报参数模型
{
  name: Ajax,
  data: [{
    "contentDownload": 7.400000002235174,
    "ttfb": 60.70000000181608,
    "resourceName": "http://localhost/xxx/AjaxHandler.aspx?r=587cf1dd-b8dc-4669-84eb-543c4d57f00b",
    "entryType": "resource",
    "initiatorType": "xmlhttprequest",
    "duration": 68.7000000034459,
    "connectStart": 924.7999999934109,
    "requestId": "587cf1dd-b8dc-4669-84eb-543c4d57f00b",
    "serviceName": "GetSearchHotKeys",
    "pageId": "df393fc4-390b-4661-b4ea-002237958051",
    "pageUrl": "/xxx/index.html",
    "transferSize": "669",
    "startTime": 1531209357858,
    "downloadSpeed": 88.28652868954921
  }]
}

业务代码中调用:

// 上报服务器页面性能信息
try {
    WebPerformance.sendPerformanceInfoToServer();
} catch (e) {throw e;}

其余操做都已经封装在了 ajax文件 和 web-performance.js 文件中了,好比将 ajax 请求记录在客户端、生成前端调试页面等。

4、开发调试页面

为了便于调试和开发,咱们在模块中提供了一个调试页面,能够经过在控制台中输入命令控制这个调试页面的开启和关闭。

页面初始化完成时,会将页面信息和初始化调用的请求信息展现出来:
调试页面.PNG-55.8kB

在页面初始化完成以后,每次ajax请求的信息都会实时添加到调试页面,就像这样:
请求.PNG-18.8kB

在控制台控制调试页面的开闭:
控制.PNG-8.1kB

问题和思考

  1. 传输大小
    performance.timing.transferSize 能够用来获取传输大小,可是公司产品WebKit版本不支持,因此前端对于css、js文件的大小暂时没办法提供。而对于ajax的传输内容大小,咱们使用 Content-Length 的值。
  2. 如何准肯定义页面初始化完成的时机
    对于图片加载,咱们能够经过 window 对象的 load 事件获取图片等外部资源加载完成的时间,也能够经过一些方法去获取首屏图片加载完成的时间,可是对于页面初始化过程当中发起的多个异步请求完成时机的判断,会相对麻烦一些,主要是因为异步请求返回结果的前后顺序不定。
  3. 咱们设想在页面初始化完成的时候,在业务代码中调用方法上报信息到服务器,那么怎么肯定页面初始化完成了?
    好比页面初始化完成应当包括 关键词查询接口返回、表格内数据查询接口返回这两个ajax请求完成,此时咱们才认为页面初始化完成了(对于这个页面来说,也能够说是首屏加载完成)。可是异步请求的返回顺序是不定的,也许查询关键字的请求先返回,也许查询表格数据的接口先返回,若是须要准肯定义初始化完成的时机,就要判断是否全部初始化涉及的请求均已成功,特别是有些页面的初始加载可能会调用不少个ajax请求,这就不太好肯定何时是初始化完成的时候。

    对于试验项目中的这个页面,由于初始化只涉及两个请求,相对来讲做为主体内容的表格数据是主要的请求,而关键词的请求相对来讲不过重要,所以咱们能够粗略的将请求表格数据成功的时间,认为是页面初始化完成的时机,咱们能够在请求表格数据的成功回调中进行信息的上报。

    可是这样显然是不够精确的,而且这个页面的初始化过程涉及的异步请求比较少,可是若是是请求数量比较多的状况呢?

    咱们的解决方案是:$.when() + $.Deferred()

    咱们使用变量接收初始化过程当中调用的 ajax 请求所返回的 jqXHR 对象,在 jQuery1.5 版本以后,$.ajax() 方法返回的 jqXHR 对象都是 Deferred 对象,所以咱们能够将这些 jqXHR 对象放在 $.when() 方法中,为它们指定回调函数(即上报服务器的操做),这样就能够保证页面初始化时机的准确性。

    代码示例以下:

    // 页面初始化
    $(function () {
      // 表格初始化
      var dtd = tableSection.showTable();
      // 设置关键字
      var dtd2 = integratedQuery.setHotKeyWords();
      $.when(dtd, dtd2)
        .done(function () {
          // 将页面性能数据上报服务器
          try {
            WebPerformance.sendPerformanceInfoToServer();
          } catch (e) {
            throw e;
          }
        })
        .fail(function () {
          console.log('fail: send performance info')
        });
        // 其余初始化操做
        // ...
     });
相关文章
相关标签/搜索