目前前端性能監控系統大致為分兩類:以GA為代表的代碼監控和以webpagetest為代表的工具監控。
代碼監控依托於js代碼並部署到需監控的頁面,手動計算時間差或者使用浏覽器的的API進行數據統計。
影響代碼監控數據的因素有以下幾種:
工具監控不用將統計代碼部署到頁面中,一般依托於虛擬機。以webpageTest為例,輸入需統計的url並且選擇運行次url的浏覽器版本,webpageTest後台虛擬機對url進行請求分析後便可以給出各種性能指標,比如瀑布流、靜態文件數量、首屏渲染時間等等。
代碼監控和工具監控的對比如下表:
根據目前業務需求以及成本預算,最終決定采用代碼監控方案。以下分別介紹代碼監控各方面的實現細節。
前端性能統計的數據大致有以下幾個:
下面介紹幾種以上幾個數據的統計方案。
使用注入代碼監控的方式統計以上指標,在沒有一些浏覽器新API(如下文將提到的timing API)的支持下,得到的數據大都是估值,雖然不准確,但也有一定的參考價值。
白屏時間節點指的是從用戶進入網站(輸入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的時間,並非准確的白屏時間。
首屏時間的統計比較復雜,目前應用比較廣的方案是將首屏的圖片、iframe等資源添加onload事件,獲取最慢的一個。
這種方案比較適合首屏元素數量固定的頁面,比如移動端首屏不論屏幕大小都展示相同數量的內容,響應式得改變內容的字體、尺寸等。但是對於首屏元素不固定的頁面,這種方案並不適用,最典型的就是PC端頁面,不同屏幕尺寸下展示的首屏內容不同。上述方案便不適用於此場景。
用戶可操作的時間節點即dom ready觸發的時間,使用jquery可以通過$(document).ready()
獲取此數據,如果不使用jQuery可以參考這裡通過原生方法實現dom ready。
總下載時間即window.onload
觸發的時間節點。
目前大多數web產品都有異步加載的內容,比如圖片的lazyload等。如果總下載時間需要統計到這些數據,可以借鑒AOP的理念,在請求異步內容之前和之後分別打點,最後計算差值。不過通常來講,我們說的總下載時間並不包括異步加載的內容。
window.performance
APIwindow.performance
是W3C性能小組引入的新的API,目前IE9以上的浏覽器都支持。一個performance對象的完整結構如下圖所示:
memory
字段代表JavaScript對內存的占用。
navigation
字段統計的是一些網頁導航相關的數據:
redirectCount
:重定向的數量(只讀),但是這個接口有同源策略限制,即僅能檢測同源的重定向;最重要的是timing
字段的統計數據,它包含了網絡、解析等一系列的時間數據。
timing
APItiming
的整體結構如下圖所示:
各字段的含義如下:
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事件觸發和結束的時間節點
可以使用Navigation.timing
統計到的時間數據來計算一些頁面性能指標,比如DNS查詢耗時、白屏時間、domready等等。如下:
Resource timing API是用來統計靜態資源相關的時間信息,詳細的內容請參考W3C Resource timing。這裡我們只介紹performance.getEntries
方法,它可以獲取頁面中每個靜態資源的請求,如下:
可以看到performance.getEntries
返回一個數組,數組的每個元素代表對應的靜態資源的信息,比如上圖展示的第一個元素對應的資源類型initiatorType
是圖片img
,請求花費的時間就是duration
的值。
關於Resource timing API的使用場景,感興趣的同學可以深入研究。
JavaScript異常一般有兩方面:語法錯誤和運行時錯誤。兩種錯誤的捕獲和處理方式不同,從而影響具體的方案選型。通常來說,處理JS異常的方案有兩種:try...catch
捕獲 和 window.onerror
捕獲。以下就兩種方案分別分析各自的優劣。
雖然語法錯誤本應該在開發構建階段使用測試工具避免,但難免會有馬失前蹄部署到線上的時候。
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
也是非常難看的並且容易引發一些不可預估的問題。
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每種浏覽器的實現有略微差異,導致需統計的信息有局限性。
為了提高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監聽捕獲到詳細的異常信息。