wangdan0 2019-07-01
通过以上几篇文章,可以对前端性能相关的概念和 API 有一个整体的认识。
前段时间和同事一起对网页性能监控方面的知识做了些探讨和实践,期望可以对用户的网络情况、程序的性能状况等做个统计分析,从而对程序进行有针对性的优化。为此我们做了个简单的试验项目,主要对 页面加载 和 ajax请求 两个方面进行了分析。(本文的方案主要是出于技术探讨的目的,只是一个 Demo,而非完整的性能监控方案)
这个图是最初的方案图,我们初级版本的程序设计基本上就是按照图上这个思路来的。
我们的实现思路是,在页面初始化完成后,将本次页面加载的信息和用户上次页面操作过程中发出的ajax请求信息上报给服务器,由服务端进行进一步统计分析。
页面加载信息,主要指css样式表、js脚本和图片等外部资源加载用时和初始化完成的时间(全部完成用时)。
用户上次页面操作过程中发出的ajax请求,主要是指用户上一次在这个页面上进行的查询、自定义设置等操作过程中,触发的ajax请求相关的信息,比如方法名称、服务器处理时间、客户端下载时间等。
为什么是用户上次操作的ajax相关信息?
主要是出于减少请求的目的,以避免监控程序本身对程序主体性能的影响,因此不会将每个请求的信息都实时的上报服务器,而是先存储在客户端。我们会将用户在这个页面进行的各种操作触发的异步请求信息,以一定格式存储在客户端 localstorage,当用户再次打开这个页面的时候,我们会从 localstorage 中取出存储的ajax信息,将其上报服务器,然后清空 localstorage 中这些旧的数据,以便重新进行记录。
因此,用户在打开这个页面时,我们上报的是用户上次的使用信息。(如果有用户只打开过一次这个页面,后面就再没使用过,那么这是一个低频使用客户,不在我们统计范围内。)
而用户的页面加载信息,每次用户打开页面时,我们都会将其上传至服务器,不需要在客户端进行存储。
服务端收到前端上报的数据后,会进行相应的分析处理,这里不对这部分进行说明。
我们要对网页的性能进行统计分析,首先应当确定哪些因素会对网页的性能带来影响。一般来说,前端HTML文档的结构是否合理,外部资源是否进行了压缩合并,静态内容是否使用了CDN加速,服务端是否配置了负载均衡,是否采取了缓存策略,以及客户端带宽状况等,都会对网页的性能造成影响。
参考资料: 浏览器的工作原理
上面这篇文章会帮助我们了解浏览器解析和渲染HTML文档的过程。具体的可以参见另一篇文档: 《浏览器解析渲染HTML页面的过程》
这里对以下几点进行着重说明:
JS 脚本会阻塞 HTML 文档的解析,包括 DOM 树的构建和渲染树的构建;CSS 样式表会阻塞渲染树的构建,但 DOM 树依然继续构建(除非遇到 script 标签且 css 文件此时仍未加载完成),但不会渲染绘制到页面上。
在 HTML 文档的解析过程中,解析器遇到 <script> 标记时会立即解析并执行脚本,HTML 文档的解析将被阻塞,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络抓取资源并解析和执行完成后,再继续解析后续内容。
但无论是哪种情况导致的阻塞,该加载的外部资源还是会加载,例如外部脚本、样式表和图片。HTML 文档的解析可能会被阻塞,但外部资源的加载不会被阻塞。
Chrome: Browser only allows six TCP connections per origin on HTTP 1.
Chrome 浏览器的并发连接数为 6 个,超过限制数目的请求会被阻塞。
参见《浏览器解析渲染HTML页面的过程》的 “CSS 和 JS 的处理顺序和阻塞分析”一节。
能够实现对网页性能的监控,主要是依靠 Performance API。
重点查看以下方法:
尤其是第一项,可以在控制台输出查看一下。
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')) || [];
web-performance.js
主要提供以下方法:
var wp = { generateGUID, // 生成当前页面唯一 id showInfoOnPage, // 在当前页面显示相关信息 recordAjaxInfo, // 记录页面初始化完成前的 ajax 信息, 或者打印初始化完成后的 ajax 信息到页面 sendPerformanceInfoToServer, // 上报服务器 setItemToLocalStorage, // 存储 xhr 信息到客户端 localStorage 中 getItemFromLocalStorage, // 获取客户端存储的 xhr 信息, 返回数组形式 getDesignatedXHRByRequestId, // 通过 requestId 获取特定 xhr 信息 getPageInitCompletedInfo, // 获取页面初始化完成的耗时信息 // ...... };
我们实现了性能监控模块 web-performance.js
,那么怎么在应用中使用?
如果只是实现对页面加载信息的分析,那么在业务代码中只需要引入这个模块,然后在业务代码中页面初始化完成时调用模块的方法即可。但是,如果要实现对每一个ajax请求的统计分析,就需要配合封装 ajax 文件。
封装的 ajax 文件中引入性能监控模块
var WebPerformance = require('./web-performance'); // 网页性能监控模块 var requestIdentifier = {};
每个请求生成唯一标识
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); } }); }); };
在成功的回调中对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 前端性能分析(二)。
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 请求记录在客户端、生成前端调试页面等。
为了便于调试和开发,我们在模块中提供了一个调试页面,可以通过在控制台中输入命令控制这个调试页面的开启和关闭。
页面初始化完成时,会将页面信息和初始化调用的请求信息展示出来:
在页面初始化完成之后,每次ajax请求的信息都会实时添加到调试页面,就像这样:
在控制台控制调试页面的开闭:
load
事件获取图片等外部资源加载完成的时间,也可以通过一些方法去获取首屏图片加载完成的时间,但是对于页面初始化过程中发起的多个异步请求完成时机的判断,会相对麻烦一些,主要是由于异步请求返回结果的先后顺序不定。我们设想在页面初始化完成的时候,在业务代码中调用方法上报信息到服务器,那么怎么确定页面初始化完成了?
比如页面初始化完成应当包括 关键词查询接口返回、表格内数据查询接口返回这两个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') }); // 其他初始化操作 // ... });