1. 程式人生 > >前端效能監控-window.performance(轉)

前端效能監控-window.performance(轉)

業界案例

目前前端效能監控系統大致為分兩類:以GA為代表的程式碼監控和以webpagetest為代表的工具監控。
程式碼監控依託於js程式碼並部署到需監控的頁面,手動計算時間差或者使用瀏覽器的的API進行資料統計。

影響程式碼監控資料的因素有以下幾種:

  • 瀏覽器渲染機制
  • 瀏覽器對API的實現程度,比如performance API
  • 工具監控不用將統計程式碼部署到頁面中,一般依託於虛擬機器。以webpageTest為例,輸入需統計的url並且選擇執行次url的瀏覽器版本,webpageTest後臺虛擬機器對url進行請求分析後便可以給出各種效能指標,比如瀑布流、靜態檔案數量、首屏渲染時間等等。

程式碼監控和工具監控的對比如下表:

這裡寫圖片描述

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

2 前端效能監控指標
前端效能統計的資料大致有以下幾個:

  • 白屏時間:從開啟網站到有內容渲染出來的時間節點;
  • 首屏時間:首屏內容渲染完畢的時間節點;
  • 使用者可操作時間節點:domready觸發節點;
  • 總下載時間:window.onload的觸發節點。
  • 下面介紹幾種以上幾個資料的統計方案。

2.1 常規統計方案
使用注入程式碼監控的方式統計以上指標,在沒有一些瀏覽器新API(如下文將提到的timing API)的支援下,得到的資料大都是估值,雖然不準確,但也有一定的參考價值。

2.1.1 白屏時間
白屏時間節點指的是從使用者進入網站(輸入url、重新整理、跳轉等方式)的時刻開始計算,一直到頁面有內容展示出來的時間節點。這個過程包括dns查詢、建立tcp連線、傳送首個http請求(如果使用https還要介入TLS的驗證時間)、返回html文件、html文件head解析完畢。

使用注入程式碼監控無法獲取解析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_time和start_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欄位統計的是一些網頁導航相關的資料:

redirectCount:重定向的數量(只讀),但是這個介面有同源策略限制,即僅能檢測同源的重定向;
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。

redirectStart和redirectEnd:如果頁面是由redirect而來,則redirectStart和redirectEnd分別代表redirect開始和結束的時間節點;
unloadEventStart和unloadEventEnd:如果前一個文件和請求的文件是同一個域的,則unloadEventStart和unloadEventEnd分別代表瀏覽器unload前一個文件的開始和結束時間節點。否則兩者都等於0;
fetchStart是指在瀏覽器發起任何請求之前的時間值。在fetchStart和domainLookupStart之間,瀏覽器會檢查當前文件的快取;
domainLookupStart和domainLookupEnd分別代表DNS查詢的開始和結束時間節點。如果瀏覽器沒有進行DNS查詢(比如使用了cache),則兩者的值都等於fetchStart;
connectStart和connectEnd分別代表TCP建立連線和連線成功的時間節點。如果瀏覽器沒有進行TCP連線(比如使用持久化連線webscoket),則兩者都等於domainLookupEnd;
secureConnectionStart:可選。如果頁面使用HTTPS,它的值是安全連線握手之前的時刻。如果該屬性不可用,則返回undefined。如果該屬性可用,但沒有使用HTTPS,則返回0;
requestStart代表瀏覽器發起請求的時間節點,請求的方式可以是請求伺服器、快取、本地資源等;
responseStart和responseEnd分別代表瀏覽器收到從伺服器端(或快取、本地資源)響應回的第一個位元組和最後一個位元組資料的時刻;
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文件完全解析完畢的時間節點;
loadEventStart和loadEventEnd分別代表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;
}

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屬性即可:

這樣的話,異域的test.js檔案中發生異常時便可以被當前域的onerror監聽捕獲到詳細的異常資訊。