在上一篇文章《Web 前端性能分析(一)》中,咱们对前端性能相关的知识进行了学习和探讨,而且作了一个试验性质的项目用来实践和验证,本文附上主要功能模块 - web-performance.js
的源码,做为对web前端性能分析的学习记录。javascript
可以实现对网页性能的监控,主要是依靠 Performance API。css
/** * ------------------------------------------------------------------ * 网页性能监控 * ------------------------------------------------------------------ */ (function (win) { // 兼容的数组判断方法 if (!Array.isArray) { Array.isArray = function (arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } // 模块定义 function factory() { var performance = win.performance; if (!performance) { // 当前浏览器不支持 console.log("Browser does not support Web Performance"); return; } var wp = {}; wp.pagePerformanceInfo = null; // 记录页面初始化性能信息 wp.xhrInfoArr = []; // 记录页面初始化完成前的 ajax 信息 /** * performance 基本方法 & 定义主要信息字段 * ------------------------------------------------------------------ */ // 计算首页加载相关时间 wp.getPerformanceTiming = function () { var t = performance.timing; var times = {}; //【重要】页面加载完成的时间, 这几乎表明了用户等待页面可用的时间 times.pageLoad = t.loadEventEnd - t.navigationStart; //【重要】DNS 查询时间 // times.dns = t.domainLookupEnd - t.domainLookupStart; //【重要】读取页面第一个字节的时间(白屏时间), 这能够理解为用户拿到你的资源占用的时间 // TTFB 即 Time To First Byte 的意思 times.ttfb = t.responseStart - t.navigationStart; //【重要】request请求耗时, 即内容加载完成的时间 // times.request = t.responseEnd - t.requestStart; //【重要】解析 DOM 树结构的时间 // times.domParse = t.domComplete - t.responseEnd; //【重要】用户可操做时间 times.domReady = t.domContentLoadedEventEnd - t.navigationStart; //【重要】执行 onload 回调函数的时间 times.onload = t.loadEventEnd - t.loadEventStart; // 卸载页面的时间 // times.unloadEvent = t.unloadEventEnd - t.unloadEventStart; // TCP 创建链接完成握手的时间 times.tcpConnect = t.connectEnd - t.connectStart; // 开始时间 times.startTime = t.navigationStart; return times; }; // 计算单个资源加载时间 wp.getEntryTiming = function (entry) { // entry 的时间点都是相对于 navigationStart 的相对时间 var t = entry; var times = {}; // 重定向的时间 // times.redirect = t.redirectEnd - t.redirectStart; // DNS 查询时间 // times.lookupDomain = t.domainLookupEnd - t.domainLookupStart; // TCP 创建链接完成握手的时间 // times.connect = t.connectEnd - t.connectStart; // 用户下载时间 times.contentDownload = t.responseEnd - t.responseStart; // ttfb 读取首字节的时间 等待服务器处理 times.ttfb = t.responseStart - t.requestStart; // 挂载 entry 返回 times.resourceName = entry.name; // 资源名称, 也是资源的绝对路径 times.entryType = entry.entryType; // 资源类型 times.initiatorType = entry.initiatorType; // link <link> | script <script> | redirect 重定向 times.duration = entry.duration; // 加载时间 // 记录开始时间 times.connectStart = entry.connectStart; return times; } // 根据 type 获取相应 entries 的 performanceTiming wp.getEntriesByType = function (type) { if (type === undefined) { return; } var entries = performance.getEntriesByType(type); return entries; }; /** * 页面初始化性能 * ------------------------------------------------------------------ */ // 获取文件资源加载信息 js/css/img wp.getFileResourceTimingInfo = function () { var entries = performance.getEntriesByType('resource'); var fileResourceInfo = { number: entries.length, // 加载文件数量 size: 0, // 加载文件大小 }; return fileResourceInfo; }; // 获取页面初始化完成的耗时信息 wp.getPageInitCompletedInfo = function () { // performance.now() 是相对于 navigationStart 的时间 var endTime = performance.now(); var pageInfo = this.getPerformanceTiming(); pageInfo.pageInitCompleted = endTime; pageInfo.pageUrl = win.location.pathname; pageInfo.pageId = this.currentPageId; return pageInfo; }; /** * xhr 相关 * ------------------------------------------------------------------ */ // 处理 xhr headers 信息, 获取传输大小 wp.handleXHRHeaders = function (headers) { // Convert the header string into an array of individual headers var arr = headers.trim().split(/[\r\n]+/); // Create a map of header names to values var headerMap = {}; arr.forEach(function (line) { var parts = line.split(': '); var header = parts.shift(); var value = parts.join(': '); headerMap[header] = value; }); return headerMap; }; // 获取 xhr 资源加载信息, 即全部的 ajax 请求的信息 wp.getXHRResourceTimingInfo = function () { var entries = performance.getEntriesByType('resource'); if (entries.length === 0) { return; } var xhrs = []; for (var i = entries.length - 1; i >= 0; i--) { var item = entries[i]; if (item.initiatorType && (item.initiatorType === 'xmlhttprequest')) { var requestId; if (item.name.lastIndexOf('?r=') > -1) { requestId = item.name.substring(item.name.lastIndexOf('?r=') + 3); } var xhr = this.getEntryTiming(item); if (requestId) { xhr.requestId = requestId; } xhrs.push(xhr); } } return xhrs; }; // 经过 requestId 获取特定 xhr 信息 wp.getDesignatedXHRByRequestId = function (requestId, serviceName, headers) { var entries = performance.getEntriesByType('resource'); if (entries.length === 0) { return; } var xhr; for (var i = entries.length - 1; i >= 0; i--) { var item = entries[i]; if (item.initiatorType && (item.initiatorType === 'xmlhttprequest')) { if (item.name.indexOf(requestId) > -1) { xhr = this.getEntryTiming(item); break; } } } var headerMap = this.handleXHRHeaders(headers); xhr.requestId = requestId; xhr.serviceName = serviceName; xhr.pageId = this.currentPageId; xhr.pageUrl = win.location.pathname; xhr.transferSize = headerMap['content-length']; xhr.startTime = performance.timing.navigationStart + parseInt(xhr.connectStart); xhr.downloadSpeed = (xhr.transferSize / 1024) / (xhr.contentDownload / 1000); return xhr; }; /** * 客户端存取 xhr 数据 * ------------------------------------------------------------------ */ // 存储 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'); } } } }; // 获取客户端存储的 xhr 信息, 返回数组形式 wp.getItemFromLocalStorage = function () { if (!win.localStorage) { // 当前浏览器不支持 console.log('Browser does not support localStorage'); return; } var localStorage = win.localStorage; var arrayObjectLocal = JSON.parse(localStorage.getItem('webperformance')) || []; return arrayObjectLocal; }; // 移除客户端存储的 xhr 信息 wp.removeItemFromLocalStorage = function () { if (!win.localStorage) { // 当前浏览器不支持 console.log('Browser does not support localStorage'); return; } localStorage.removeItem('webperformance'); }; /** * 工具方法 * ------------------------------------------------------------------ */ // 生成惟一标识 wp.generateGUID = function () { var d = new Date().getTime(); if (typeof performance !== 'undefined' && typeof performance.now === 'function') { d += performance.now(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); }; /** * 封装 xhr 请求 * ------------------------------------------------------------------ */ wp.ajax = (function () { var URL = '../UpdataProfilerHandler.aspx'; var ajax = function (type, input, success, error) { var data = 'name=' + type + '&data=' + escape(JSON.stringify(input)); var xhr = new XMLHttpRequest(); xhr.open('POST', URL, true); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.onreadystatechange = function () { if ((xhr.readyState == 4) && (xhr.status == 200)) { var result = JSON.parse(xhr.responseText); success && success(result); } }; xhr.send(data); }; return ajax; })(); /** * 上报服务器 * ------------------------------------------------------------------ */ // 上报服务器页面初始化性能信息 wp.sendPagePerformanceInfoToServer = function () { var pageInfo = this.getPageInitCompletedInfo(); this.showInfoOnPage(pageInfo, 'page'); // 要在记录 this.pagePerformanceInfo 以前调用 this.showInfoOnPage(this.xhrInfoArr, 'ajax'); this.pagePerformanceInfo = JSON.parse(JSON.stringify(pageInfo)); try { this.ajax('Page', pageInfo, function () { console.log('send page performance info success') }); } catch (e) { throw e; } }; // 上报服务器 xhr 信息 wp.sendXHRPerformanceInfoToServer = function () { var xhrInfo = this.getItemFromLocalStorage(); if (!xhrInfo || xhrInfo.length === 0) { return; } try { this.ajax('Ajax', xhrInfo, function () { console.log('send ajax performance info success') wp.removeItemFromLocalStorage(); }); } catch (e) { throw e; } }; // 上报服务器 wp.sendPerformanceInfoToServer = function () { if (this.pagePerformanceInfo) { return; } this.sendPagePerformanceInfoToServer(); this.sendXHRPerformanceInfoToServer(); }; /** * 当前页面数据展现(开发调试用) * ------------------------------------------------------------------ */ // 页面信息描述 // var pageInfoDescribe = { // pageLoad: '加载用时(ms)', // pageInitCompleted: '初始化完成(ms)' // }; // 请求信息描述 // var xhrInfoDescribe = { // serviceName: '服务名称', // ttfb: '服务器处理(ms)', // contentDownload: '数据下载(ms)', // transferSize: '数据大小(byte)', // downloadSpeed: '下载速度(kb/s)' // }; // 记录页面初始化完成前的 ajax 信息, 或者打印初始化完成后的 ajax 信息到页面 wp.recordAjaxInfo = function (xhr) { if (!this.pagePerformanceInfo) { this.xhrInfoArr.push(xhr); } else { this.showInfoOnPage(xhr, 'action'); } }; // 在当前页面显示相关信息 wp.showInfoOnPage = function (info, type) { // 若是传入参数为空或调试开关未打开 return if (!win.localStorage.getItem('windProfiler') || !info) { return; } info = JSON.parse(JSON.stringify(info)); var debugInfo = document.getElementById(this.currentPageId); if (debugInfo === null) { debugInfo = document.createElement('div'); debugInfo.id = this.currentPageId; debugInfo.className = 'debuginfo'; document.body.appendChild(debugInfo); var style = document.createElement('style'); style.type = "text/css"; style.innerHTML = 'div.debuginfo{' + 'background-color: #000;' + 'color: #fff;' + 'border: 1px solid sliver;' + 'padding: 5px;' + 'width: 500px;' + 'height: 300px;' + 'position: absolute;' + 'right: 10px;' + 'bottom: 10px;' + 'overflow: auto;' + 'z-index: 9999;' + '}' + 'div.debuginfo table th, td{' + 'padding: 5px;' + '}'; document.getElementsByTagName('head').item(0).appendChild(style); } var title, message, table = '', th = '', td = '', tableHead = '<table style="border-collapse: separate;" border="1">', tableEnd = '</table>'; if (type === 'page') { title = '页面信息'; th += '<tr><th>加载用时(ms)</th><th>初始化完成(ms)</th></tr>'; td += '<tr><td>' + info.pageLoad.toFixed(2) + '</td><td>' + info.pageInitCompleted.toFixed(2) + '</td></tr>'; } else if (type === 'ajax') { title = '请求信息(初始化)'; th += '<tr><th>服务名称</th><th>服务器耗时</th><th>下载耗时</th><th>数据大小</th><th>下载速度(kb/s)</th></tr>'; for (var i = 0; i < info.length; i++) { td += '<tr><td>' + info[i].serviceName + '</td><td>' + info[i].ttfb.toFixed(2) + '</td><td>' + info[i].contentDownload.toFixed(2) + '</td><td>' + info[i].transferSize + '</td><td>' + info[i].downloadSpeed.toFixed(2) + '</td></tr>'; } } else if (type === 'action') { title = '请求信息(用户操做)'; td += '<td>' + info.serviceName + '</td><td>' + info.ttfb.toFixed(2) + '</td><td>' + info.contentDownload.toFixed(2) + '</td><td>' + info.transferSize + '</td><td>' + info.downloadSpeed.toFixed(2) + '</td>'; var actionTable = debugInfo.querySelector('.action'); if (actionTable === null) { var html = '<table class="action" style="border-collapse: separate;" border="1">'; html += '<tr><th>服务名称</th><th>服务器耗时</th><th>下载耗时</th><th>数据大小</th><th>下载速度(kb/s)</th></tr>'; html += '<tr>' + td + '</tr>'; html += '</table>'; debugInfo.innerHTML += '<p>' + title + '</p>'; debugInfo.innerHTML += html; } else { var tr = actionTable.insertRow(-1); tr.innerHTML = td; } return; } table += tableHead + th + td + tableEnd; debugInfo.innerHTML += '<p>' + title + '</p>'; debugInfo.innerHTML += table + '<br>'; }; /** * 对外接口, 控制调试页面的开关 * ------------------------------------------------------------------ */ performance.windProfiler = (function (win) { var profiler = { openClientDebug: function () { try { win.localStorage.setItem('windProfiler', 'debug'); console.log('调试已打开,请刷新页面'); } catch (e) { throw e; } }, closeClientDebug: function () { try { win.localStorage.removeItem('windProfiler'); console.log('调试已关闭'); } catch (e) { throw e; } } }; return profiler; })(win); /** * 事件绑定 * ------------------------------------------------------------------ */ // 监听 DOMContentLoaded 事件, 获取文件资源加载信息 win.document.addEventListener('DOMContentLoaded', function (event) { // var resourceTimingInfo = wp.getFileResourceTimingInfo(); }); // 监听 load 事件, 获取 PerformanceTiming 信息 win.addEventListener('load', function (event) { // setTimeout(function () { // wp.sendPagePerformanceInfoToServer(); // }, 0); }); // 生成当前页面惟一 id wp.currentPageId = wp.generateGUID(); return wp; } /** * 模块导出, 兼容 CommonJS AMD 及 原生JS * ------------------------------------------------------------------ */ if (typeof module === "object" && typeof module.exports === "object") { module.exports = factory(); } else if (typeof define === "function" && define.amd) { define(factory); } else { win.WebPerformance = factory(); } })(typeof window !== 'undefined' ? window : global);
/** * 封装 jquery ajax * 例如: * ajaxRequest.ajax.triggerService( * 'apiCommand', [命令数据] ) * .then(successCallback, failureCallback); * ); */ var WebPerformance = require('./web-performance'); // 网页性能监控模块 var JSON2 = require('LibsDir/json2'); var URL = '../AjaxSecureHandler.aspx?r='; var requestIdentifier = {}; var ajaxRequest = ajaxRequest || {}; (function ($) { if (!$) { throw 'jquery获取失败!'; } ajaxRequest.json = JSON2; 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) { 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); } }); }); }; $.extend(ajaxRequest.ajax, { defaultOpts: { // url: '../AjaxSecureHandler.aspx', dataType: 'json', type: 'POST', contentType: 'application/x-www-form-urlencoded; charset=UTF-8' }, handleResponse: function (result, $dfd, jqXHR, userOptions, serviceName, requestId) { if (!result) { $dfd && $dfd.reject(jqXHR, 'error response format!'); userOptions.error(jqXHR, 'error response format!'); return; } if (result.ErrorCode != '200') { // 服务器已经错误 $dfd && $dfd.reject(jqXHR, result.ErrorMessage); userOptions.error(jqXHR, result); return; } try { // 将这次请求的信息存储到客户端的 localStorage var headers = jqXHR.getAllResponseHeaders(); var xhr = WebPerformance.getDesignatedXHRByRequestId(requestId, serviceName, headers); WebPerformance.setItemToLocalStorage(xhr); WebPerformance.recordAjaxInfo(xhr); // 要在成功的回调以前调用 } catch (e) {throw e} if (result.Data) { // 将大于2^53的数字(16位以上)包裹双引号,避免溢出 var jsonStr = result.Data.replace(/(:\s*)(\d{16,})(\s*,|\s*})/g, '$1"$2"$3'); var resultData = ajaxRequest.json.parse(jsonStr); $dfd.resolve(resultData); userOptions.success && userOptions.success(resultData); } else { $dfd.resolve(); userOptions.success && userOptions.success(); } }, buildServiceRequest: function (serviceName, input, userSuccess, userError, ajaxParams) { var requestData = { MethodAlias: serviceName, Parameter: input }; var request = $.extend({}, ajaxParams, { data: 'data=' + escape(ajaxRequest.json.stringify(requestData)), success: userSuccess, error: function (jqXHR, textStatus, errorThrown) { console.log(serviceName, jqXHR); if (userError && (typeof userError === 'function')) { userError(jqXHR, textStatus, errorThrown); } } }); return request; }, 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); } }); })(jQuery); module.exports = ajaxRequest;