简要说明
在上一篇文章《Web 前端性能分析(一)》中,我们对前端性能相关的知识进行了学习和探讨,并且做了一个试验性质的项目用来实践和验证,本文附上主要功能模块 – web-performance.js 的源码,作为对 web 前端性能分析的学习记录。
Performance API
能够实现对网页性能的监控,主要是依靠 Performance API。
《JavaScript 标准参考教程(alpha)》
MDN 文档
模块源码
web-performance.js
/**
* ——————————————————————
* 网页性能监控
* ——————————————————————
*/
(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);
ajax-request.js
/**
* 封装 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;