Web 前端性能分析(二)

簡要說明

在上一篇文章《Web 前端性能分析(一)》中,咱們對前端性能相關的知識進行了學習和探討,而且作了一個試驗性質的項目用來實踐和驗證,本文附上主要功能模塊 - web-performance.js 的源碼,做爲對web前端性能分析的學習記錄。javascript

Performance API

可以實現對網頁性能的監控,主要是依靠 Performance API。css

  1. 《JavaScript 標準參考教程(alpha)》
  2. 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;
相關文章
相關標籤/搜索