前端性能監控方案window.performance 調研(轉)

1. 業界案例

目前前端性能監控系統大體爲分兩類:以GA爲表明的代碼監控和以webpagetest爲表明的工具監控。javascript

代碼監控依託於js代碼並部署到需監控的頁面,手動計算時間差或者使用瀏覽器的的API進行數據統計。php

影響代碼監控數據的因素有如下幾種:css

  • 瀏覽器渲染機制;
  • 瀏覽器對API的實現程度,好比performance API;

工具監控不用將統計代碼部署到頁面中,通常依託於虛擬機。以webpageTest爲例,輸入需統計的url而且選擇運行次url的瀏覽器版本,webpageTest後臺虛擬機對url進行請求分析後即可以給出各類性能指標,好比瀑布流、靜態文件數量、首屏渲染時間等等。html

代碼監控和工具監控的對好比下表:
前端

根據目前業務需求以及成本預算,最終決定採用代碼監控方案。如下分別介紹代碼監控各方面的實現細節。html5

2. 前端性能監控指標

前端性能統計的數據大體有如下幾個:java

  • 白屏時間:從打開網站到有內容渲染出來的時間節點;
  • 首屏時間:首屏內容渲染完畢的時間節點;
  • 用戶可操做時間節點:domready觸發節點;
  • 總下載時間:window.onload的觸發節點。

下面介紹幾種以上幾個數據的統計方案。jquery

2.1 常規統計方案

使用注入代碼監控的方式統計以上指標,在沒有一些瀏覽器新API(以下文將提到的timing API)的支持下,獲得的數據大都是估值,雖然不許確,但也有必定的參考價值。web

2.1.1 白屏時間

白屏時間節點指的是從用戶進入網站(輸入url、刷新、跳轉等方式)的時刻開始計算,一直到頁面有內容展現出來的時間節點。這個過程包括dns查詢、創建tcp鏈接、發送首個http請求(若是使用https還要介入TLS的驗證時間)、返回html文檔、html文檔head解析完畢。django

使用注入代碼監控沒法獲取解析html文檔以前的時間信息,目前廣泛使用的白屏時間統計方案是在html文檔的head中全部的靜態資源以及內嵌腳本/樣式以前記錄一個時間點,在head最底部記錄另外一個時間點,二者的差值做爲白屏時間。以下:

<html> <head> <meta charset="UTF-8"/> <!--這裏還有一大串meta信息--> <script> var start_time = new Date();//統計起點,實際爲html開始解析的時間節點 </script> <link href='a.css'></link> <script src='a.js'></script> <script> var end_time = new Date();//統計起點,實際爲html開始解析的時間節點 </script> </head> <body> </body> </html>

上述代碼中的end_timestart_time的差值通常做爲白屏時間的估值,但理論上來說,這個差值只是瀏覽器解析html文檔head的時間,並不是準確的白屏時間。

2.1.2 首屏時間

首屏時間的統計比較複雜,目前應用比較廣的方案是將首屏的圖片、iframe等資源添加onload事件,獲取最慢的一個。

這種方案比較適合首屏元素數量固定的頁面,好比移動端首屏不論屏幕大小都展現相同數量的內容,響應式得改變內容的字體、尺寸等。可是對於首屏元素不固定的頁面,這種方案並不適用,最典型的就是PC端頁面,不一樣屏幕尺寸下展現的首屏內容不一樣。上述方案便不適用於此場景。

2.1.3 可操做時間

用戶可操做的時間節點即dom ready觸發的時間,使用jquery能夠經過$(document).ready()獲取此數據,若是不使用jQuery能夠參考這裏經過原生方法實現dom ready。

2.1.4 總下載時間

總下載時間即window.onload觸發的時間節點。

目前大多數web產品都有異步加載的內容,好比圖片的lazyload等。若是總下載時間須要統計到這些數據,能夠借鑑AOP的理念,在請求異步內容以前和以後分別打點,最後計算差值。不過一般來說,咱們說的總下載時間並不包括異步加載的內容。

2.2 使用window.performance API

window.performance 是W3C性能小組引入的新的API,目前IE9以上的瀏覽器都支持。一個performance對象的完整結構以下圖所示:

memory字段表明JavaScript對內存的佔用。

navigation字段統計的是一些網頁導航相關的數據:

  1. redirectCount:重定向的數量(只讀),可是這個接口有同源策略限制,即僅能檢測同源的重定向;
  2. type 返回值應該是0,1,2 中的一個。分別對應三個枚舉值:
    • 0 : TYPE_NAVIGATE (用戶經過常規導航方式訪問頁面,好比點一個連接,或者通常的get方式)
    • 1 : TYPE_RELOAD (用戶經過刷新,包括JS調用刷新接口等方式訪問頁面)
    • 2 : TYPE_BACK_FORWARD (用戶經過後退按鈕訪問本頁面)

最重要的是timing字段的統計數據,它包含了網絡、解析等一系列的時間數據。

2.2.1 timing API

timing的總體結構以下圖所示:

各字段的含義以下:

  • startTime:有些瀏覽器實現爲navigationStart,表明瀏覽器開始unload前一個頁面文檔的開始時間節點。好比咱們當前正在瀏覽baidu.com,在地址欄輸入google.com並回車,瀏覽器的執行動做依次爲:unload當前文檔(即baidu.com)->請求下一文檔(即google.com)。navigationStart的值即是觸發unload當前文檔的時間節點。

    若是當前文檔爲空,則navigationStart的值等於fetchStart。

  • redirectStartredirectEnd:若是頁面是由redirect而來,則redirectStart和redirectEnd分別表明redirect開始和結束的時間節點;
  • unloadEventStartunloadEventEnd:若是前一個文檔和請求的文檔是同一個域的,則unloadEventStartunloadEventEnd分別表明瀏覽器unload前一個文檔的開始和結束時間節點。不然二者都等於0;
  • fetchStart是指在瀏覽器發起任何請求以前的時間值。在fetchStart和domainLookupStart之間,瀏覽器會檢查當前文檔的緩存;
  • domainLookupStartdomainLookupEnd分別表明DNS查詢的開始和結束時間節點。若是瀏覽器沒有進行DNS查詢(好比使用了cache),則二者的值都等於fetchStart
  • connectStartconnectEnd分別表明TCP創建鏈接和鏈接成功的時間節點。若是瀏覽器沒有進行TCP鏈接(好比使用持久化鏈接webscoket),則二者都等於domainLookupEnd
  • secureConnectionStart:可選。若是頁面使用HTTPS,它的值是安全鏈接握手以前的時刻。若是該屬性不可用,則返回undefined。若是該屬性可用,但沒有使用HTTPS,則返回0;
  • requestStart表明瀏覽器發起請求的時間節點,請求的方式能夠是請求服務器、緩存、本地資源等;
  • responseStartresponseEnd分別表明瀏覽器收到從服務器端(或緩存、本地資源)響應回的第一個字節和最後一個字節數據的時刻;
  • domLoading表明瀏覽器開始解析html文檔的時間節點。咱們知道IE瀏覽器下的document有readyState屬性,domLoading的值就等於readyState改變爲loading的時間節點;
  • domInteractive表明瀏覽器解析html文檔的狀態爲interactive時的時間節點。domInteractive並不是DOMReady,它早於DOMReady觸發,表明html文檔解析完畢(即dom tree建立完成)可是內嵌資源(好比外鏈css、js等)還未加載的時間點;
  • domContentLoadedEventStart:表明DOMContentLoaded事件觸發的時間節點:

    頁面文檔徹底加載並解析完畢以後,會觸發DOMContentLoaded事件,HTML文檔不會等待樣式文件,圖片文件,子框架頁面的加載(load事件能夠用來檢測HTML頁面是否徹底加載完畢(fully-loaded))。

  • domContentLoadedEventEnd:表明DOMContentLoaded事件完成的時間節點,此刻用戶能夠對頁面進行操做,也就是jQuery中的domready時間;
  • domComplete:html文檔徹底解析完畢的時間節點;
  • loadEventStartloadEventEnd分別表明onload事件觸發和結束的時間節點

2.2.2 計算性能指標

可使用Navigation.timing 統計到的時間數據來計算一些頁面性能指標,好比DNS查詢耗時、白屏時間、domready等等。以下:

  • DNS查詢耗時 = domainLookupEnd - domainLookupStart
  • TCP連接耗時 = connectEnd - connectStart
  • request請求耗時 = responseEnd - responseStart
  • 解析dom樹耗時 = domComplete - domInteractive
  • 白屏時間 = domloadng - fetchStart
  • domready時間 = domContentLoadedEventEnd - fetchStart
  • onload時間 = loadEventEnd - fetchStart
2.2.3 Resource timing API

Resource timing API是用來統計靜態資源相關的時間信息,詳細的內容請參考W3C Resource timing。這裏咱們只介紹performance.getEntries方法,它能夠獲取頁面中每一個靜態資源的請求,以下:

能夠看到performance.getEntries返回一個數組,數組的每一個元素表明對應的靜態資源的信息,好比上圖展現的第一個元素對應的資源類型initiatorType是圖片img,請求花費的時間就是duration的值。

關於Resource timing API的使用場景,感興趣的同窗能夠深刻研究。

 

// 計算加載時間
function getPerformanceTiming () {  
    var performance = window.performance;
 
    if (!performance) {
        // 當前瀏覽器不支持
        console.log('你的瀏覽器不支持 performance 接口');
        return;
    }
 
    var t = performance.timing;
    var times = {};
 
    //【重要】頁面加載完成的時間
    //【緣由】這幾乎表明了用戶等待頁面可用的時間
    times.loadPage = t.loadEventEnd - t.navigationStart;
 
    //【重要】解析 DOM 樹結構的時間
    //【緣由】檢討下你的 DOM 樹嵌套是否是太多了!
    times.domReady = t.domComplete - t.responseEnd;
 
    //【重要】重定向的時間
    //【緣由】拒絕重定向!好比,http://example.com/ 就不應寫成 http://example.com
    times.redirect = t.redirectEnd - t.redirectStart;
 
    //【重要】DNS 查詢時間
    //【緣由】DNS 預加載作了麼?頁面內是否是使用了太多不一樣的域名致使域名查詢的時間太長?
    // 可以使用 HTML5 Prefetch 預查詢 DNS ,見:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)            
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
 
    //【重要】讀取頁面第一個字節的時間
    //【緣由】這能夠理解爲用戶拿到你的資源佔用的時間,加異地機房了麼,加CDN 處理了麼?加帶寬了麼?加 CPU 運算速度了麼?
    // TTFB 即 Time To First Byte 的意思
    // 維基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
    times.ttfb = t.responseStart - t.navigationStart;
 
    //【重要】內容加載完成的時間
    //【緣由】頁面內容通過 gzip 壓縮了麼,靜態資源 css/js 等壓縮了麼?
    times.request = t.responseEnd - t.requestStart;
 
    //【重要】執行 onload 回調函數的時間
    //【緣由】是否太多沒必要要的操做都放到 onload 回調函數裏執行了,考慮過延遲加載、按需加載的策略麼?
    times.loadEvent = t.loadEventEnd - t.loadEventStart;
 
    // DNS 緩存時間
    times.appcache = t.domainLookupStart - t.fetchStart;
 
    // 卸載頁面的時間
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
 
    // TCP 創建鏈接完成握手的時間
    times.connect = t.connectEnd - t.connectStart;
 
    return times;
}

  

2.3 參考資料

  1. Facebook測速方案
  2. Measuring Page Load Speed with Navigation Timing;
  3. 前端數據之美 -- 基礎篇;
  4. 7 天打造前端性能監控系統;
  5. phantomjs ;
  6. 前端相關數據監控
  7. 研究首屏時間?你先要知道這幾點細節

3. JavaScript代碼異常監控

JavaScript異常通常有兩方面:語法錯誤和運行時錯誤。兩種錯誤的捕獲和處理方式不一樣,從而影響具體的方案選型。一般來講,處理JS異常的方案有兩種:try...catch捕獲 和 window.onerror捕獲。如下就兩種方案分別分析各自的優劣。

雖然語法錯誤本應該在開發構建階段使用測試工具避免,但不免會有馬失前蹄部署到線上的時候。

3.1 try...catch捕獲

這種方案要求開發人員在編寫代碼的時候,在預估有異常發生的代碼段使用try...catch,在發生異常時將異常信息發送給接口:

try{ //可能發生異常的代碼段 }catch(e){ //將異常信息發送服務端 }

try...catch的優勢是能夠細化到每一個代碼塊,而且能夠自定義錯誤信息以便統計。

具體到上文提到的兩種js異常,try...catch沒法捕獲語法錯誤,當遇到語法錯誤時,瀏覽器仍然會拋出錯誤Uncaught SyntaxError,可是不會被捕獲,不會走進catch的代碼塊內。

另外,若是try代碼塊中有回調函數也不會被捕獲,好比:

try{ var btn = $('#btn'); btn.on('click',function(){ //throw error }); }catch(e){}

上述代碼中btn的監聽函數裏拋出的異常沒法被外層的catch捕獲到,必須額外套一層:

try{ var btn = $('#btn'); btn.on('click',function(){ try{ //throw error }catch(e){} }); }catch(e){}

綜上所述,try...catch方案的部署很是複雜,若是人工部署除了要求巨量的工做量,還跟開發人員的能力和經驗有關。若是依賴編譯工具部署(好比fis),那每一個代碼塊都套一層try...catch也是很是難看的而且容易引起一些不可預估的問題。

3.2 window.onerror捕獲

這種方式不須要開發人員在代碼中書寫大量的try...catch,經過給window添加onerror監聽,在js發生異常的時候即可以捕獲到錯誤信息,語法異常和運行異常都可被捕獲到。可是window.onerror這個監聽必須放在全部js文件以前才能夠保證可以捕獲到全部的異常信息。

window.onerror事件的詳細信息參考這裏

/** * @param {String} errorMessage 錯誤信息 * @param {String} scriptURL 出錯文件的URL * @param {Long} lineNumber 出錯代碼的行號 * @param {Long} columnNumber 出錯代碼的列號 * @param {Object} errorObj 錯誤信息Object */ window.onerror = function(errorMessage, scriptURL, lineNumber,columnNumber,errorObj) { // code.. }

onerror的實現方式各瀏覽器略有差別,可是前三個參數都是相同的,某些低版本瀏覽器沒有後兩個參數。

最後一個參數errorObj各瀏覽器實現的程度不一致,具體可參考這裏

下圖是被onerror捕獲到的一個異常的具體信息:

綜上所述,window.onerror方案的優勢是減小了開發人員的工做量,部署方便,而且能夠捕獲語法錯誤和運行錯誤。缺點是錯誤信息不能自定義,而且errorObj每種瀏覽器的實現有略微差別,致使需統計的信息有侷限性。

3.3 跨域JS文件異常的捕獲

爲了提升web性能,目前大部分web產品架構中都有CDN這一環,將資源部署到不一樣的域名上,充分利用瀏覽器的併發請求機制。那麼在跨域JS文件中發生異常的時候,onerror監聽會捕獲到什麼信息呢?請看下圖:

只有一個稍微有價值的信息Script error,其餘什麼信息都沒有,爲何會這樣呢?

咱們都知道瀏覽器有同源資源限制,常規狀態下是沒法進行跨域請求的。而script、img、iframe標籤的src屬性是沒有這種限制的,這也是不少跨域方案的基礎。可是即便script標籤能夠請求到異域的js文件,此文件中的信息也並不能暴露到當前域內,這也是瀏覽器的安全措施所致。

那麼有沒有辦法獲取到異域資源的異常信息呢?

其實很簡單,目前能夠說基本上全部的web產品對於js/css/image等靜態資源都在服務端設置了Access-Control-Allow-Origin: *的響應頭,也就是容許跨域請求。在這個環境下,只要咱們在請求跨域資源的script標籤上添加一個crossorigin屬性便可:

<script src="http://static.toutiao.com/test.js" crossorigin></script>

這樣的話,異域的test.js文件中發生異常時即可以被當前域的onerror監聽捕獲到詳細的異常信息。

3.4 參考資料

  1. 構建web前端異常監控系統;
  2. 前端代碼異常日誌收集與監控;
  3. 前端代碼異常監控
相關文章
相關標籤/搜索