在上一篇文章《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;