目前前端性能監控系統大體爲分兩類:以GA爲表明的代碼監控和以webpagetest爲表明的工具監控。javascript
代碼監控依託於js代碼並部署到需監控的頁面,手動計算時間差或者使用瀏覽器的的API進行數據統計。php
影響代碼監控數據的因素有如下幾種:css
工具監控不用將統計代碼部署到頁面中,通常依託於虛擬機。以webpageTest爲例,輸入需統計的url而且選擇運行次url的瀏覽器版本,webpageTest後臺虛擬機對url進行請求分析後即可以給出各類性能指標,好比瀑布流、靜態文件數量、首屏渲染時間等等。html
代碼監控和工具監控的對好比下表:
前端
根據目前業務需求以及成本預算,最終決定採用代碼監控方案。如下分別介紹代碼監控各方面的實現細節。html5
前端性能統計的數據大體有如下幾個:java
下面介紹幾種以上幾個數據的統計方案。jquery
使用注入代碼監控的方式統計以上指標,在沒有一些瀏覽器新API(以下文將提到的timing API)的支持下,獲得的數據大都是估值,雖然不許確,但也有必定的參考價值。web
白屏時間節點指的是從用戶進入網站(輸入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_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的使用場景,感興趣的同窗能夠深刻研究。
// 計算加載時間 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; }
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監聽捕獲到詳細的異常信息。