Web 前端性能分析(一)

參考連接

  1. 初探 performance – 監控網頁與程序性能
  2. 使用簡潔的 Navigation Timing API 測試網頁加載速度
  3. 前端性能統計
  4. 前端性能——監控起步
  5. 使用性能API快速分析web前端性能
  6. Page Visibility

經過以上幾篇文章,能夠對前端性能相關的概念和 API 有一個總體的認識。javascript

簡要說明

前段時間和同事一塊兒對網頁性能監控方面的知識作了些探討和實踐,指望能夠對用戶的網絡狀況、程序的性能情況等作個統計分析,從而對程序進行有針對性的優化。爲此咱們作了個簡單的試驗項目,主要對 頁面加載ajax請求 兩個方面進行了分析。(本文的方案主要是出於技術探討的目的,只是一個 Demo,而非完整的性能監控方案)css

Web前端統計.PNG-40.3kB

這個圖是最初的方案圖,咱們初級版本的程序設計基本上就是按照圖上這個思路來的。html

咱們的實現思路是,在頁面初始化完成後,將本次頁面加載的信息用戶上次頁面操做過程當中發出的ajax請求信息上報給服務器,由服務端進行進一步統計分析。前端

頁面加載信息,主要指css樣式表、js腳本和圖片等外部資源加載用時和初始化完成的時間(所有完成用時)。
用戶上次頁面操做過程當中發出的ajax請求,主要是指用戶上一次在這個頁面上進行的查詢、自定義設置等操做過程當中,觸發的ajax請求相關的信息,好比方法名稱、服務器處理時間、客戶端下載時間等。html5

爲何是用戶上次操做的ajax相關信息?
主要是出於減小請求的目的,以免監控程序自己對程序主體性能的影響,所以不會將每一個請求的信息都實時的上報服務器,而是先存儲在客戶端。咱們會將用戶在這個頁面進行的各類操做觸發的異步請求信息,以必定格式存儲在客戶端 localstorage,當用戶再次打開這個頁面的時候,咱們會從 localstorage 中取出存儲的ajax信息,將其上報服務器,而後清空 localstorage 中這些舊的數據,以便從新進行記錄。java

所以,用戶在打開這個頁面時,咱們上報的是用戶上次的使用信息。(若是有用戶只打開過一次這個頁面,後面就再沒使用過,那麼這是一個低頻使用客戶,不在咱們統計範圍內。)jquery

而用戶的頁面加載信息,每次用戶打開頁面時,咱們都會將其上傳至服務器,不須要在客戶端進行存儲。web

服務端收到前端上報的數據後,會進行相應的分析處理,這裏不對這部分進行說明。ajax

相關知識

1、影響網頁性能的因素

  1. HTML 的解析和渲染(參見文檔 《瀏覽器解析渲染HTML頁面的過程》
  2. 服務端處理的速度(負載均衡,緩存策略)
  3. 客戶端帶寬(網絡情況)

咱們要對網頁的性能進行統計分析,首先應當肯定哪些因素會對網頁的性能帶來影響。通常來講,前端HTML文檔的結構是否合理,外部資源是否進行了壓縮合並,靜態內容是否使用了CDN加速,服務端是否配置了負載均衡,是否採起了緩存策略,以及客戶端帶寬情況等,都會對網頁的性能形成影響。segmentfault

2、瀏覽器解析渲染HTML頁面的過程

參考資料: 瀏覽器的工做原理

上面這篇文章會幫助咱們瞭解瀏覽器解析和渲染HTML文檔的過程。具體的能夠參見另外一篇文檔: 《瀏覽器解析渲染HTML頁面的過程》

這裏對如下幾點進行着重說明:

  1. HTML 文檔的解析和渲染是一個漸進的過程。爲達到更好的用戶體驗,呈現引擎會力求儘快將內容顯示在屏幕上。它沒必要等到整個 HTML 文檔解析完畢,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其他內容的同時,呈現引擎會將部份內容解析並顯示出來。
  2. 瀏覽器的預解析機制。
  3. HTML 文檔的解析和渲染過程當中,外部樣式表和腳本順序執行、併發加載

JS 腳本會阻塞 HTML 文檔的解析,包括 DOM 樹的構建和渲染樹的構建;CSS 樣式表會阻塞渲染樹的構建,但 DOM 樹依然繼續構建(除非遇到 script 標籤且 css 文件此時仍未加載完成),但不會渲染繪製到頁面上。
在 HTML 文檔的解析過程當中,解析器遇到 <script> 標記時會當即解析並執行腳本,HTML 文檔的解析將被阻塞,直到腳本執行完畢。若是腳本是外部的,那麼解析過程會中止,直到從網絡抓取資源並解析和執行完成後,再繼續解析後續內容。
但不管是哪一種狀況致使的阻塞,該加載的外部資源仍是會加載,例如外部腳本、樣式表和圖片。HTML 文檔的解析可能會被阻塞,但外部資源的加載不會被阻塞。

3、瀏覽器併發鏈接數

Chrome: Browser only allows six TCP connections per origin on HTTP 1.

Chrome 瀏覽器的併發鏈接數爲 6 個,超過限制數目的請求會被阻塞。

參見《瀏覽器解析渲染HTML頁面的過程》的 「CSS 和 JS 的處理順序和阻塞分析」一節。

4、Performance API

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

  1. 《JavaScript 標準參考教程(alpha)》
  2. MDN文檔

重點查看如下方法:

  1. Performance.timing
  2. Performance.getEntries()
  3. Performance.getEntriesByType()
  4. Performance.now()

尤爲是第一項,能夠在控制檯輸出查看一下。

5、localStorage

  1. Web Storage API
  2. calculating-usage-of-localstorage-space

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')) || [];

網頁性能指標

1、頁面性能指標

  1. 白屏時間
    讀取頁面首字節時間(ttfb - Time To First Byte),能夠理解爲用戶拿到頁面資源佔用的時間。
    瀏覽器對html文檔的解析和渲染是一個漸進的過程,通常在拿到首字節以後便會有內容繪製在頁面上,正常網絡狀態下基本上白屏時間很短。
  2. 資源加載
    瀏覽器在接收到服務器返回的 html 文檔數據以後,會起一系列的線程去請求文檔解析中遇到的各類資源,js腳本、CSS樣式表、圖片,以及發起異步請求。咱們這裏的資源認爲是 js/css/圖片,後面統計資源加載狀況時,會統計這些資源的文件大小、文件數量、總的加載用時。ajax異步請求咱們會另外進行統計。
  3. 用戶可操做時間
    在查閱相關資料時,會看到用戶等待頁面時間、用戶可操做時間等概念,不一樣資料和文章的定義也不一樣,這裏咱們認爲用戶可操做時間就是用戶能夠進行頁面操做的時間,此時 html 文檔解析完成(domContentLoadedEventEnd)。另外一種用戶等待頁面的時間,通常是按照頁面加載完成的時間來統計(loadEventEnd)。但在咱們此次的前端性能監控方案中,並不將其做爲主要的監控指標。
  4. 首屏渲染時間
    首屏時間的統計比較複雜,由於涉及圖片資源的下載及異步請求等因素。有些資料統計中不計算圖片的下載時間,但咱們認爲既然是首屏的展現,應當包括圖片加載的完成。判斷首屏圖片加載完成的方法,這裏再也不詳述,能夠查閱相關文章。咱們此次的前端性能分析方案中,並無涉及到圖片,而是關注頁面初始化過程當中的異步請求。

2、ajax 請求性能指標

  1. 服務器處理時間
  2. 客戶端下載時間
  3. 接口名稱
  4. 下載速度
  5. 頁面路徑及id
  6. 傳輸大小

代碼說明

1、模塊構成

web-performance.js

  1. 兼容 CommonJS AMD CMD 及 原生JS
  2. 無第三方依賴(好比jquery)
  3. 主要提供如下方法:

    var wp = {
        generateGUID, // 生成當前頁面惟一 id
        showInfoOnPage, // 在當前頁面顯示相關信息
        recordAjaxInfo, // 記錄頁面初始化完成前的 ajax 信息, 或者打印初始化完成後的 ajax 信息到頁面
        sendPerformanceInfoToServer, // 上報服務器
        setItemToLocalStorage, // 存儲 xhr 信息到客戶端 localStorage 中
        getItemFromLocalStorage, // 獲取客戶端存儲的 xhr 信息, 返回數組形式
        getDesignatedXHRByRequestId, // 經過 requestId 獲取特定 xhr 信息
        getPageInitCompletedInfo, // 獲取頁面初始化完成的耗時信息
        // ......
    };

2、與業務代碼的結合

咱們實現了性能監控模塊 web-performance.js,那麼怎麼在應用中使用?
若是隻是實現對頁面加載信息的分析,那麼在業務代碼中只須要引入這個模塊,而後在業務代碼中頁面初始化完成時調用模塊的方法便可。可是,若是要實現對每個ajax請求的統計分析,就須要配合封裝 ajax 文件。

  1. 封裝的 ajax 文件中引入性能監控模塊

    var WebPerformance = require('./web-performance'); // 網頁性能監控模塊
    var requestIdentifier = {};
  2. 每一個請求生成惟一標識

    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);
              }
            });
        });
      };
  3. 在成功的回調中對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 前端性能分析(二)

3、接口調用

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 請求記錄在客戶端、生成前端調試頁面等。

4、開發調試頁面

爲了便於調試和開發,咱們在模塊中提供了一個調試頁面,能夠經過在控制檯中輸入命令控制這個調試頁面的開啓和關閉。

頁面初始化完成時,會將頁面信息和初始化調用的請求信息展現出來:
調試頁面.PNG-55.8kB

在頁面初始化完成以後,每次ajax請求的信息都會實時添加到調試頁面,就像這樣:
請求.PNG-18.8kB

在控制檯控制調試頁面的開閉:
控制.PNG-8.1kB

問題和思考

  1. 傳輸大小
    performance.timing.transferSize 能夠用來獲取傳輸大小,可是公司產品WebKit版本不支持,因此前端對於css、js文件的大小暫時沒辦法提供。而對於ajax的傳輸內容大小,咱們使用 Content-Length 的值。
  2. 如何準肯定義頁面初始化完成的時機
    對於圖片加載,咱們能夠經過 window 對象的 load 事件獲取圖片等外部資源加載完成的時間,也能夠經過一些方法去獲取首屏圖片加載完成的時間,可是對於頁面初始化過程當中發起的多個異步請求完成時機的判斷,會相對麻煩一些,主要是因爲異步請求返回結果的前後順序不定。
  3. 咱們設想在頁面初始化完成的時候,在業務代碼中調用方法上報信息到服務器,那麼怎麼肯定頁面初始化完成了?
    好比頁面初始化完成應當包括 關鍵詞查詢接口返回、表格內數據查詢接口返回這兩個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')
        });
        // 其餘初始化操做
        // ...
     });
相關文章
相關標籤/搜索