作為一名開發來講,以下場景你有沒有遇到:
- 點選這個按鈕怎麼沒反應了
- 頁面為什麼白了
- 怎麼一直正在載入
- 很多使用者說圖片載入不出來
- ......
那麼有一款效能監控產品太重要了,但是效能相關的東西實在太多了。那麼從一個熟悉又陌生的api開始,performance。
1.什麼是performance
mdn上是這麼解釋的:Performance
介面可以獲取到當前頁面中與效能相關的資訊。它是 High Resolution Time API 的一部分,同時也融合了 Performance Timeline API、Navigation Timing API、User Timing API 和 Resource Timing API。
直接在控制檯列印:
除去eventCounts和memory相容不是很好的屬性,重點看下剩餘四個。
2.performace.timing屬性
我們配合這張經典圖來了解這些屬性
頁面進來之前:
navigationStart:前一個網頁解除安裝時間。
unloadEventStart:前一個網頁的upload事件開始
unloadEventEnd:前一個網頁的upload事件結束
redirectStart:網頁重定向開始時間
redirectEnd:網頁重定向結束時間
頁面進來之後:
fetchStart:開始請求網頁
domainLookupStart:dns查詢開始
domainLookupEnd:dns查詢結束
connectStart:向伺服器建立握手開始
connectEnd:向伺服器建立握手結束
secureConnectionStart:安全握手開始,預設值0。非https的沒有
requestStart:向伺服器傳送請求開始
responseStart:伺服器返回資料開始
responseEnd:伺服器返回資料結束
解析dom開始:
domLoading:解析dom開始,document.readyState為loading
domInteractive:解析dom結束,document.readyState為interactive
domContentLoadedEventStart:DomContentLoaden所有回撥開始執行
domContentLoadedEventEnd:ContentLoaded結束,dom內容載入完畢
loadEventStart:load事件發生前
loadEventEnd:load事件發生後
3.實戰一波
有了上邊的具體資料,如何進行計算進行監控上報呢。
以下程式碼中的p均為performance.timing物件
3.1 網路連線相關
// 上一個頁面的時間,沒多大用途 const pervPage = p.fetchStart - p.navigationStart // 重定向時間 const redirect = p.redirectEnd - p.redirectStart // dns查詢時間 const dns = p.domainLookupEnd - p.domainLookupStart // tcp建立時間 const connect = p.connectEnd - p.connectStart // 網路總耗時 const network = p.connectEnd - p.navigationStart
網路建連層如果太慢或者出問題,那麼首先應該和運維部門溝通,查詢問題,和前端後端關聯性不大。
3.2 網路接收相關
// 前端傳送到接收的時間 const send = p.responseStart - p.requestStart // 接收資料用時 const receive = p.responseEnd - p.responseStart // 總耗時 const request = p.responseEnd - p.requestStart
3.3 前端渲染
// dom解析時間 const dom = p.domComplete - p.domLoading // loadEvent時間 const loadEvent = p.loadEventEnd - p.loadEventStart // 總耗時 const frontend = p.loadEventEnd - p.domLoading
3.4 關鍵階段
// 頁面完全載入的時間 const load = p.loadEventEnd - p.navigationStart // dom準備時間 const domReady = p.domainLookupStart - p.navigationStart // 可操作時間 const interactive = p.domInteractive - p.navigationStart // 首位元組時間 const ttfb = p.responseStart - p.navigationStart
3.5 使用
// dom解析完成 let isDOMReady = false; // callback 就是獲取上邊計算performance各個指標的回撥 function domready (callback) { if (isDOMReady) return; let timer = null; let runCheck = () => { if (performance.timing.domComplete) { clearTimeout(timer); callback(); isDOMReady = true; // 1、停止迴圈檢測 2、執行callback } else { // 再去迴圈檢測 timer = setTimeout(runCheck, 100); } } if (document.readyState === "interactive") { callback(); return; } document.addEventListener('DOMContentLoaded', () => { // 開始迴圈檢測 是否 DOMContentLoaded 已經完成了 runCheck(); }) }
// 在onload事件中 let isOnload = false; function onload (callback) { if (isOnload) return; let timer = null; let runCheck = () => { if (performance.timing.loadEventEnd) { clearTimeout(timer); callback(); isOnload = true; // 1、停止迴圈檢測 2、執行callback } else { // 再去迴圈檢測 timer = setTimeout(runCheck, 100); } } if (document.readyState === "interactive") { callback(); return; } window.addEventListener('load', () => { runCheck(); }) }
在domready和onload方法中,可以避免出現負數問題。
4. 資源監控
performance提供了getEnteries方法,用來獲取載入的資原始檔,返回陣列形式。
同樣的,我們也可以封裝個方法,根據這些屬性,計算出想要的資訊。程式碼中r代表陣列某一項
const resourceData = { initiatorType: r.initiatorType, name: r.name, duration: parseInt(r.duration), // 連線過程 redirect: r.redirectEnd - r.redirectStart, // 重定向的時間 dns: r.domainLookupEnd - r.domainLookupStart, // dns查詢的時間 connect: r.connectEnd - r.connectStart, // tcp連線的時間 network: r.connectEnd - r.startTime, // 網路總耗時 // 接收過程 send: r.responseStart - r.requestStart, // 傳送開始到接收的總時長 receive: r.responseEnd - r.responseStart, // 接收的總時長 request: r.responseEnd - r.requestStart, // 接收的總耗時 // 核心指標 ttfb: r.responseStart - r.requestStart, // 首位元組時間 }
但是,如果我們開發是一個商城網站,裡邊會有很多css、js、img等資源,他們都值得監控嗎?
可以封裝一個方法,傳入link、script、或者圖片名稱包含包含某些欄位的,或者隨機抽取幾個,我們去自定義去監聽規則。有興趣可以搜尋一下大量日誌log上報策略。
5. ajax請求監控
在實戰專案中,肯定會涉及到網路請求和後端的互動,如何監聽ajax請求的相關資料呢?
透過修改XMLHttpRequest原型,自定義open和send方法來達到目的。
const ajax = () => { let xhr = window.XMLHttpRequest; if (xhr._eagle_monitor_flag) return; xhr._eagle_monitor_flag = true; let _originOpen = xhr.prototype.open; xhr.prototype.open = function (method, url, async, user, password) { // 往xhr上自定義屬性 this._eagle_xhr_info = { url, method, status: null } return _originOpen.apply(this, arguments); } let _originSend = xhr.prototype.send; xhr.prototype.send = function () { let _self = this; this._eagle_start_time = Date.now(); let ajaxEnd = (eventType) => () => { if (_self.response) { // 統計返回資料的大小 let responseSize = null; switch (_self.responseType) { case 'json': // JSON相容性問題 && stringify報錯 responseSize = JSON.stringify(_self.response).length; break; case 'arraybuffer': responseSize = _self.response.byteLength; break; default: responseSize = _self.responseText.length; // 簡單使用responseText的值 } _self._eagle_xhr_info.event = eventType; _self._eagle_xhr_info.status = _self.status; _self._eagle_xhr_info.success = _self.status === 200; _self._eagle_xhr_info.duration = Date.now() = _self._eagle_start_time; _self._eagle_xhr_info.responseSize = responseSize; _self._eagle_xhr_info.requestSize = value ? value.length : 0 // value一定有length?百度查一下相容寫法; _self._eagle_xhr_info.type = 'xhr'; // 列印_eagle_xhr_info,進行額外操作 // console.log(_self._eagle_xhr_info) // callback(_self._eagle_xhr_info) } }; // 這三種狀態都代表著請求已經結束了 // 需要統計一些資訊,並進行上報 this.addEventListener('load', ajaxEnd('load'), false); this.addEventListener('error', false); this.addEventListener('abort', false); return _originSend.apply(this, arguments); } }