經過以上幾篇文章,能夠對前端性能相關的概念和 API 有一個總體的認識。javascript
前段時間和同事一塊兒對網頁性能監控方面的知識作了些探討和實踐,指望能夠對用戶的網絡狀況、程序的性能情況等作個統計分析,從而對程序進行有針對性的優化。爲此咱們作了個簡單的試驗項目,主要對 頁面加載 和 ajax請求 兩個方面進行了分析。(本文的方案主要是出於技術探討的目的,只是一個 Demo,而非完整的性能監控方案)css
這個圖是最初的方案圖,咱們初級版本的程序設計基本上就是按照圖上這個思路來的。html
咱們的實現思路是,在頁面初始化完成後,將本次頁面加載的信息和用戶上次頁面操做過程當中發出的ajax請求信息上報給服務器,由服務端進行進一步統計分析。前端
頁面加載信息,主要指css樣式表、js腳本和圖片等外部資源加載用時和初始化完成的時間(所有完成用時)。
用戶上次頁面操做過程當中發出的ajax請求,主要是指用戶上一次在這個頁面上進行的查詢、自定義設置等操做過程當中,觸發的ajax請求相關的信息,好比方法名稱、服務器處理時間、客戶端下載時間等。html5
爲何是用戶上次操做的ajax相關信息?
主要是出於減小請求的目的,以免監控程序自己對程序主體性能的影響,所以不會將每一個請求的信息都實時的上報服務器,而是先存儲在客戶端。咱們會將用戶在這個頁面進行的各類操做觸發的異步請求信息,以必定格式存儲在客戶端 localstorage,當用戶再次打開這個頁面的時候,咱們會從 localstorage 中取出存儲的ajax信息,將其上報服務器,而後清空 localstorage 中這些舊的數據,以便從新進行記錄。java
所以,用戶在打開這個頁面時,咱們上報的是用戶上次的使用信息。(若是有用戶只打開過一次這個頁面,後面就再沒使用過,那麼這是一個低頻使用客戶,不在咱們統計範圍內。)jquery
而用戶的頁面加載信息,每次用戶打開頁面時,咱們都會將其上傳至服務器,不須要在客戶端進行存儲。web
服務端收到前端上報的數據後,會進行相應的分析處理,這裏不對這部分進行說明。ajax
咱們要對網頁的性能進行統計分析,首先應當肯定哪些因素會對網頁的性能帶來影響。通常來講,前端HTML文檔的結構是否合理,外部資源是否進行了壓縮合並,靜態內容是否使用了CDN加速,服務端是否配置了負載均衡,是否採起了緩存策略,以及客戶端帶寬情況等,都會對網頁的性能形成影響。segmentfault
參考資料: 瀏覽器的工做原理
上面這篇文章會幫助咱們瞭解瀏覽器解析和渲染HTML文檔的過程。具體的能夠參見另外一篇文檔: 《瀏覽器解析渲染HTML頁面的過程》
這裏對如下幾點進行着重說明:
JS 腳本會阻塞 HTML 文檔的解析,包括 DOM 樹的構建和渲染樹的構建;CSS 樣式表會阻塞渲染樹的構建,但 DOM 樹依然繼續構建(除非遇到 script 標籤且 css 文件此時仍未加載完成),但不會渲染繪製到頁面上。
在 HTML 文檔的解析過程當中,解析器遇到 <script> 標記時會當即解析並執行腳本,HTML 文檔的解析將被阻塞,直到腳本執行完畢。若是腳本是外部的,那麼解析過程會中止,直到從網絡抓取資源並解析和執行完成後,再繼續解析後續內容。
但不管是哪一種狀況致使的阻塞,該加載的外部資源仍是會加載,例如外部腳本、樣式表和圖片。HTML 文檔的解析可能會被阻塞,但外部資源的加載不會被阻塞。
Chrome: Browser only allows six TCP connections per origin on HTTP 1.
Chrome 瀏覽器的併發鏈接數爲 6 個,超過限制數目的請求會被阻塞。
參見《瀏覽器解析渲染HTML頁面的過程》的 「CSS 和 JS 的處理順序和阻塞分析」一節。
可以實現對網頁性能的監控,主要是依靠 Performance API。
重點查看如下方法:
尤爲是第一項,能夠在控制檯輸出查看一下。
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')) || [];
web-performance.js
主要提供如下方法:
var wp = { generateGUID, // 生成當前頁面惟一 id showInfoOnPage, // 在當前頁面顯示相關信息 recordAjaxInfo, // 記錄頁面初始化完成前的 ajax 信息, 或者打印初始化完成後的 ajax 信息到頁面 sendPerformanceInfoToServer, // 上報服務器 setItemToLocalStorage, // 存儲 xhr 信息到客戶端 localStorage 中 getItemFromLocalStorage, // 獲取客戶端存儲的 xhr 信息, 返回數組形式 getDesignatedXHRByRequestId, // 經過 requestId 獲取特定 xhr 信息 getPageInitCompletedInfo, // 獲取頁面初始化完成的耗時信息 // ...... };
咱們實現了性能監控模塊 web-performance.js
,那麼怎麼在應用中使用?
若是隻是實現對頁面加載信息的分析,那麼在業務代碼中只須要引入這個模塊,而後在業務代碼中頁面初始化完成時調用模塊的方法便可。可是,若是要實現對每個ajax請求的統計分析,就須要配合封裝 ajax 文件。
封裝的 ajax 文件中引入性能監控模塊
var WebPerformance = require('./web-performance'); // 網頁性能監控模塊 var requestIdentifier = {};
每一個請求生成惟一標識
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); } }); }); };
在成功的回調中對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 前端性能分析(二)。
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 請求記錄在客戶端、生成前端調試頁面等。
爲了便於調試和開發,咱們在模塊中提供了一個調試頁面,能夠經過在控制檯中輸入命令控制這個調試頁面的開啓和關閉。
頁面初始化完成時,會將頁面信息和初始化調用的請求信息展現出來:
在頁面初始化完成以後,每次ajax請求的信息都會實時添加到調試頁面,就像這樣:
在控制檯控制調試頁面的開閉:
load
事件獲取圖片等外部資源加載完成的時間,也能夠經過一些方法去獲取首屏圖片加載完成的時間,可是對於頁面初始化過程當中發起的多個異步請求完成時機的判斷,會相對麻煩一些,主要是因爲異步請求返回結果的前後順序不定。咱們設想在頁面初始化完成的時候,在業務代碼中調用方法上報信息到服務器,那麼怎麼肯定頁面初始化完成了?
好比頁面初始化完成應當包括 關鍵詞查詢接口返回、表格內數據查詢接口返回這兩個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') }); // 其餘初始化操做 // ... });