前端資料監控一般分為效能資料監控和線上異常監控。本文對這兩塊資料的監控原理和方法進行整理說明。
效能資料
統計方案
- 程式碼監控
- 將監控程式碼注入到頁面中,手動計算時間差或者使用瀏覽器API進行資料統計。
- 工具監控
- 不將統計程式碼注入到頁面中,一般藉助虛擬機器對頁面進行效能資料分析。
型別 | 優點 | 缺點 | 示例 |
---|---|---|---|
非侵入式 | 指標齊全、客戶端主動監測、競品監控 | 無法知道效能影響使用者數、取樣少容易失真、無法監控複雜應用與細分功能 | Pagespeed、PhantomJS、UAQ |
侵入式 | 真實海量使用者資料、能監控複雜應用與業務功能、使用者點選與區域渲染 | 需插入指令碼統計、網路指標不全、無法監控競品 | DP 、Google 統計 |
在進行效能資料監控之前,先要明確頁面從使用者開始訪問到頁面載入完成經歷的時間階段。
時間階段
按觸發順序排列所有屬性:(更詳細標準的解釋請參看:W3C Editor's Draft)
-
navigationStart:在同一個瀏覽器上下文中,前一個網頁(與當前頁面不一定同域)unload 的時間戳,如果無前一個網頁 unload ,則與 fetchStart 值相等
-
unloadEventStart:前一個網頁(與當前頁面同域)unload 的時間戳,如果無前一個網頁 unload 或者前一個網頁與當前頁面不同域,則值為 0
-
unloadEventEnd:和 unloadEventStart 相對應,返回前一個網頁 unload 事件繫結的回撥函式執行完畢的時間戳
-
redirectStart:第一個 HTTP 重定向發生時的時間。有跳轉且是同域名內的重定向才算,否則值為 0
-
redirectEnd:最後一個 HTTP 重定向完成時的時間。有跳轉且是同域名內的重定向才算,否則值為 0
-
fetchStart:瀏覽器準備好使用 HTTP 請求抓取文件的時間,這發生在檢查本地快取之前
-
domainLookupStart:DNS 域名查詢開始的時間,如果使用了本地快取(即無 DNS 查詢)或持久連線,則與 fetchStart 值相等
-
domainLookupEnd:DNS 域名查詢完成的時間,如果使用了本地快取(即無 DNS 查詢)或持久連線,則與 fetchStart 值相等
-
connectStart:HTTP(TCP) 開始建立連線的時間,如果是持久連線,則與 fetchStart 值相等,如果在傳輸層發生了錯誤且重新建立連線,則這裡顯示的是新建立的連線開始的時間
-
connectEnd:HTTP(TCP) 完成建立連線的時間(完成握手),如果是持久連線,則與 fetchStart 值相等,如果在傳輸層發生了錯誤且重新建立連線,則這裡顯示的是新建立的連線完成的時間
注意:這裡握手結束,包括安全連線建立完成、SOCKS 授權通過
-
secureConnectionStart:HTTPS 連線開始的時間,如果不是安全連線,則值為 0
-
requestStart:HTTP 請求讀取真實文件開始的時間(完成建立連線),包括從本地讀取快取,連線錯誤重連時,這裡顯示的也是新建立連線的時間
-
responseStart:HTTP 開始接收響應的時間(獲取到第一個位元組),包括從本地讀取快取
-
responseEnd:HTTP 響應全部接收完成的時間(獲取到最後一個位元組),包括從本地讀取快取
-
domLoading:開始解析渲染 DOM 樹的時間,此時 Document.readyState 變為 loading,並將丟擲 readystatechange 相關事件
-
domInteractive:完成解析 DOM 樹的時間,Document.readyState 變為 interactive,並將丟擲 readystatechange 相關事件
注意:只是 DOM 樹解析完成,這時候並沒有開始載入網頁內的資源
-
domContentLoadedEventStart:DOM 解析完成後,網頁內資源載入開始的時間,文件發生 DOMContentLoaded事件的時間
-
domContentLoadedEventEnd:DOM 解析完成後,網頁內資源載入完成的時間(如 JS 指令碼載入執行完畢),文件的DOMContentLoaded 事件的結束時間
-
domComplete:DOM 樹解析完成,且資源也準備就緒的時間,Document.readyState 變為 complete,並將丟擲 readystatechange 相關事件
-
loadEventStart:load 事件傳送給文件,也即 load 回撥函式開始執行的時間,如果沒有繫結 load 事件,值為 0
-
loadEventEnd:load 事件的回撥函式執行完畢的時間,如果沒有繫結 load 事件,值為 0
DOMContentLoaded 和 load 事件的區別,詳見 DOMContentLoaded與load的區別。
統計方法
Navigation Timing API
可以直接使用Navigation Timing介面來獲取統計起點以及載入過程中的各個階段耗時。window.performance 是W3C效能小組引入的新的API,目前IE9以上的瀏覽器都支援。
常用計算:
- DNS查詢耗時 :domainLookupEnd - domainLookupStart
- TCP連結耗時 :connectEnd - connectStart
- request請求耗時 :responseEnd - responseStart
- 解析dom樹耗時 : domComplete - domInteractive 白屏時間 :responseStart - navigationStart
- domready時間(使用者可操作時間節點) :domContentLoadedEventEnd - navigationStart
- onload時間(總下載時間) :loadEventEnd - navigationStart
Resource timing API
Resource timing API是用來統計靜態資源相關的時間資訊,詳細的內容請參考W3C Resource timing。這裡我們只介紹performance.getEntries方法
指標及計算方法
指標
- 總體指標
- 到 DOM 可互動耗時timing.domComplete - timing.navigationStart
- 總耗時timing.loadEventEnd - timing.navigationStart 到 DNS 查詢結束耗時timing.domainLookupEnd - timing.navigationStart
- 到請求結束耗時timing.responseEnd - timing.navigationStart
- 首次渲染耗時timing.msFirstPaint
- 階段指標(按順序)
- 重定向時間timing.redirectEnd - timing.redirectStart
- unload 事件時間timing.unloadEventEnd - timing.unloadEventStart
- appcache 時間timing.domainLookupStart - timing.fetchStart
- DNS 查詢時間timing.domainLookupEnd - timing.domainLookupStart
- 連線時間timing.connectEnd - timing.connectStart
- 請求時間timing.responseEnd - timing.requestStart
- 請求到 DOM 可互動時間(包含解析HTML,非defer的script和css的時間)timing.domInteractive - timing.responseEnd
- DOM 可互動到 DOMReady 時間(包含處理defer的script的時間)timing.domComplete - * timing.domInteractive
- load 事件時間timing.loadEventEnd - timing.loadEventStart
詳見原始碼:timing
關鍵指標
- 白屏時間(first paint time)- 使用者從開啟頁面到頁面開始有內容呈現為止
- 首屏時間 - 使用者從開啟頁面到首屏內所有內容都呈現出來所花費的時間
- 使用者可操作時間(dom interactive) - 使用者從開啟頁面到可以進行正常點選、輸入等操作的時間
- 總下載時間 - 使用者從開啟頁面到頁面所有資源都載入完畢並呈現出來所化的時間
確定統計起點
我們需要在使用者輸入 URL 或者點選連結的時候就開始統計,因為這樣才能衡量使用者的等待時間。如果你的使用者高階瀏覽器佔比很高,那麼可以直接使用Navigation Timing介面來獲取統計起點以及載入過程中的各個階段耗時。
白屏時間
白屏時間是使用者首次看到內容的時間,也叫做首次渲染時間,chrome 高版本有 firstPaintTime 介面來獲取這個耗時,但大部分瀏覽器並不支援,必須想其他辦法來監測。仔細觀察 WebPagetest 檢視分析發現,白屏時間出現在頭部外鏈資源載入完附近,因為瀏覽器只有載入並解析完頭部資源才會真正渲染頁面。基於此我們可以通過獲取頭部資源載入完的時刻來近似統計白屏時間。儘管並不精確,但卻考慮了影響白屏的主要因素:首位元組時間和頭部資源載入時間。
如何統計頭部資源載入呢?我們發現頭部內嵌的 JS 通常需等待前面的 JS\CSS 載入完才會執行,是不是可以在瀏覽器 head 內底部加一句 JS 統計頭部資源載入結束點呢?可以通過一個簡單的示例進行測試:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8"/>
<script>
var start_time = +new Date; //測試時間起點,實際統計起點為 DNS 查詢
</script>
<!-- 3s 後這個 js 才會返回 -->
<script src="script.php"></script>
<script>
var end_time = +new Date; //時間終點
var headtime = end_time - start_time; //頭部資源載入時間
console.log(headtime);
</script>
</head>
<body>
<p>在頭部資源載入完之前頁面將是白屏</p>
<p>script.php 被模擬設定 3s 後返回,head 底部內嵌 JS 等待前面 js 返回後才執行</p>
<p>script.php 替換成一個執行長時間迴圈的 js 效果也一樣</p>
</body>
</html>
複製程式碼
經測試發現,統計的頭部載入時間正好跟頭部資源下載時間相近,而且換成一個執行時間很長的 JS 也會等到 JS 執行完才統計。說明此方法是可行的(具體原因可檢視瀏覽器渲染原理及 JS 單執行緒相關介紹)。
除了上述方法,還可以採用 navigation timing api 來獲取白屏時間。該方法的缺點是瀏覽器相容性較差,在 safari 等瀏覽器中無法使用。
var firstPaint = 0;
// Chrome
if (window.chrome && window.chrome.loadTimes) {
// Convert to ms
firstPaint = window.chrome.loadTimes().firstPaintTime * 1000;
api.firstPaintTime = firstPaint - timing.navigationStart;
}
// IE
else if (typeof timing.msFirstPaint === 'number') {
firstPaint = timing.msFirstPaint;
api.firstPaintTime = firstPaint - timing.navigationStart;
}
// Firefox
// This will use the first times after MozAfterPaint fires
//else if (timing.navigationStart && typeof InstallTrigger !== 'undefined') {
// api.firstPaint = timing.navigationStart;
// api.firstPaintTime = mozFirstPaintTime - timing.navigationStart;
//}
複製程式碼
首屏時間
在首屏渲染之前埋上處理邏輯,使用定時器不斷的去檢測img節點的圖片。判斷圖片是否在首屏和載入完成,找到首屏中載入時間最慢的的圖片完成的時間,從而計算出首屏時間。如果首屏有沒有圖片,如果沒圖片就用domready時間。統計流程如下:
首屏位置呼叫 API 開始統計 -> 繫結首屏內所有圖片的 load 事件 -> 頁面載入完後判斷圖片是否在首屏內,找出載入最慢的一張 -> 首屏時間
複製程式碼
這是同步載入情況下的簡單統計邏輯,另外需要注意的幾點:
- 頁面存在 iframe 的情況下也需要判斷載入時間
- gif 圖片在 IE 上可能重複觸發 load 事件需排除
- 非同步渲染的情況下應在非同步獲取資料插入之後再計算首屏
- css 重要背景圖片可以通過 JS 請求圖片 url 來統計(瀏覽器不會重複載入)
- 沒有圖片則以統計 JS 執行時間為首屏,即認為文字出現時間
統計使用者可操作
使用者可操作預設可以統計domready時間,因為通常會在這時候繫結事件操作。對於使用了模組化非同步載入的 JS 可以在程式碼中去主動標記重要 JS 的載入時間,這也是產品指標的統計方式。
performance.timing.domInteractive - performance.timing.navigationStart
複製程式碼
總下載
總下載時間預設可以統計onload時間,這樣可以統計同步載入的資源全部載入完的耗時。如果頁面中存在很多非同步渲染,可以將非同步渲染全部完成的時間作為總下載時間。
performance.timing.loadEventStart- performance.timing.navigationStart
複製程式碼
線上異常
介面異常監控
方法很簡單,在介面出錯的情況下主動打點上報錯誤。
JS 程式碼異常監控
- try catch 捕獲
這種方案要求開發人員在編寫程式碼的時候,在預估有異常發生的程式碼段使用try...catch,在發生異常時將異常資訊傳送給介面:
try{
//可能發生異常的程式碼段
}catch(e){
//將異常資訊傳送服務端
}
複製程式碼
- window.onerror捕獲
這種方式不需要開發人員在程式碼中書寫大量的try...catch,通過給window新增onerror監聽,在js發生異常的時候便可以捕獲到錯誤資訊,語法異常和執行異常均可被捕獲到。但是window.onerror這個監聽必須放在所有js檔案之前才可以保證能夠捕獲到所有的異常資訊。
- 跨域JS檔案異常的捕獲
目前可以說基本上所有的web產品對於js/css/image等靜態資源都在服務端設定了Access-Control-Allow-Origin: *的響應頭,也就是允許跨域請求。在這個環境下,只要我們在請求跨域資源的script標籤上新增一個crossorigin屬性即可:
<script src="http://static.toutiao.com/test.js" crossorigin></script>
複製程式碼
這樣的話,異域的test.js檔案中發生異常時便可以被當前域的onerror監聽捕獲到詳細的異常資訊。