? 性能監控 在前端一直是一個口頭上備受關注但開發中又常被忽略的點,畢竟不是每個開發者很容易就做到的事。好在HTML5新增了performance
特性,它是High Resolution Time API 的一部分,目的在于獲取到當前頁面中與性能相關的信息,以便幫助開發者直觀感受頁面性能及針對問題優化。
? 了解如何監控頁面性能前,我們先回顧幾個指標:
(1)白屏時間:頁面被打開,到首字節渲染呈現所需的時間。
(2)首屏時間:首屏內容渲染完成所需的時間。
(3)下載時間(HTTP請求耗時):頁面所需資源從服務器上下載完成所需的時間。
(4)DOM樹解析時間:資源下載完成到頁面構建展示出來所需的時間。
? ...
? 這些信息都如何獲取?在此標準之前,也有一些手段可以實現,但H5的performance
直接來源于瀏覽器,與手工Date.time,Cookie等對比,使用上更方便,數據上更準確。(Date.now()
會受程序阻塞影響)
?
$ Performance 屬性
? 關于performance屬性,建議讀者自己在工具編輯器上直接打印出來看看更能真切的體會該接口。本文主要介紹前兩者,對其他內容感興趣的同學,可以 戳這里
-
.timing
(只讀)
:對象;包含了延遲相關的性能信息。 -
.navigation
(只讀)
:對象;包含了指定的時間段里發生的操作相關信息,包括頁面是加載還是刷新、發生了多少次重定向等等。 -
.timeOrigin
(只讀)
:即將失效。用于返回性能測量開始時的高精度時間戳。 -
.memory:由chrome拓展的非標準屬性,用于返回基本內存的使用情況。注意非chrome不支持。
?
# Performance.timing 只讀
const PerformanceTiming = window.performance.timing
? 返回值為一個對象,記錄著完整的頁面加載信息。其各個節點如下:
? 看著上圖,回顧一下一般意義的頁面加載過程:瀏覽器向服務器請求資源
--> DOM結構解析
--> 構建DOM樹
--> 構建CSS規則樹
--> 構建渲染樹
--> 繪制頁面
。可以看出,這個過程只是上圖中的某一小部分,我們來詳談一下實際的整個過程
-
Prompt for unload 階段
-
.navigationStart
:瀏覽器完成卸載前一個文檔的時間。如果沒前一個文檔,則該值與第三步.fetchStart
的值相同。 -
.unloadEventStart
:返回前一個同源文檔出發卸載(unload)事件前的時間。如果沒有前一個文檔,或前文檔與本文檔不同源,或需重定向,則返回0。 -
.unloadEventEnd
:返回前一個同源文檔完成卸載的時間。如果沒有或文檔不同源,則返回0.
-
-
Redirect 階段
-
.redirectStart
:http重定向開始的時間。如果中間有多個重定向,且每個重定向均同源,則返回第一個重定向的.fetchStart
時間,若不同源,則為0 -
.redirectEnd
:http重定向結束時間。如果中間有多個重定向且均同源,則返回最后一個重定向結束時間。若不同源,則為0。
-
-
App cache 階段
-
.fetchStart
:瀏覽器準備好使用HTTP請求來獲取(fetch
)文檔的時間,這個時間會在檢查任何應用緩存之前。
-
-
DNS查詢階段
-
.domainLookupStart
:用戶代理對當前文檔所屬域進行DNS查詢開始的時間。如果是長連接(如websocket
),或本地緩存了,則該值與.fetchStart
相同 -
.domainLookupEnd
:域名查詢結束的時間。如果是長連接,或本地緩存了,則該值與.fetchStart
相同
-
-
TCP連接階段
-
.connectStart
:用戶代理開始向服務器請求所需文檔時,連接建立的開始時間。如果是長連接,或本地緩存了,則該值與.fetchStart
相同 -
.secureConnectStart
:返回與服務器開始SSL握手時的時間。異常情況同上。 -
.connectEnd
: HTTP握手成功,認證結束,連接建立時的時間。如果是長連接,或本地緩存了,則該值與.fetchStart
相同。
-
-
Request 階段
-
requestStart
:從服務器/緩存/本地資源中開始請求文檔的時間。如果連接發生斷開重連,該信息會被刷新。 - 沒有請求結束時間是因為該動作發生在服務器端,且受數據鏈路等各個因素影響,瀏覽器并不能準確反饋該信息
-
-
Response 階段
-
.responseStart
:從服務器/緩存/本地資源中接收到第一個字節時的時間。如果連接發生斷開重連,該信息會被刷新。 -
.responseEnd
:從服務器/緩存/本地資源中接收到最后一個字節時的時間。如果連接提前關閉,則返回提前關閉的時間。獲取該值時需注意要在Response結束之后,如window.onload
,否則可能不準確。
-
-
Processing 執行階段
-
.domLoading
:資源下載完成,開始解析DOM結構,當Document.readyState
的值更新為loading
時的時間。 -
.domInteractive
:DOM解析完成,開始加載內嵌資源,即Document.readyState
的值更新為interactive
時的時間 - 執行階段內的 DOMContentLoaded 階段
-
.domContentLoadedEventStart
:解析器發送DOMContentLoaded
事件,所有需要被執行的腳本均解析完成時的時間。 -
.domContentLoadedEventEnd
:所有立即執行的腳本均執行完成時的時間。不執行的腳本如懶加載資源不在該范圍內。
-
-
.domComplete
:當前文檔解析完成,document.readyState
的值更新為complete
時的時間。
-
-
load 業務涉入階段
-
.loadEventStart
:文檔觸發load事件的時間,如果還沒觸發,則返回0。 -
.loadEventEnd
:文檔結束load事件的時間,未觸發則返回0。
?
-
# 性能監控指標
? 通過以上的各個事件分析,不難得出如下各個時間段:
const timing = window.performance.timing
-
DNS解析耗時:
timing.domainLookupEnd - timing.domainLookupStart
-
TCP連接耗時:
timing.connectEnd - timing.connectStart
-
發送請求耗時:
timing.responseStart - timing.requestStart
-
接收請求耗時:
timing.responseEnd - timing.responseStart
-
解析DOM耗時:
timing.domInteractive - timing.domLoading
-
頁面加載完成:
timing.domContentLoadedEventStart - timing.domInteractive
-
DOMContentLoaded事件耗時:
timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart
-
DOM加載完成:
timing.domComplete - timing.domContentLoadedEventEnd
-
DOMLoad事件耗時:
timing.loadEventEnd - timing.loadEventStart
? 除此之外,在文首提到的其他幾個性能指標,如下:
-
白屏時間:
timing.responseStart - timing.navigationStart
-
首屏時間:
timing.domComplete- timing.navigationStart
-
資源下載總耗時:
timing.responseEnd - timing.requestStart;
-
請求完畢至DOM加載:
timing.domInteractive - timing.responseEnd
?
# 實戰案例
? 封裝一個函數如下,注釋前半部為參數功能,后半部為監控到頁面性能問題時可能的原因
function getPerformanceTiming () {
var performance = window.performance
// 瀏覽器兼容性考慮
if(!performance) {
console.log('您的瀏覽器不支持 performance 接口')
return
}
const t = performance.timing
let times = {}
// 頁面加載完成時間 - 用戶需等待頁面可用時間
times.loadPage = t.loadEventEnd - t.navigationStart
// 解析dom樹結構時間 - DOM樹嵌套不宜太深
times.domReady = t.domeComplete - t.responseEnd
// 重定向時間 - 若拒絕重定向,檢查是否有類似‘http://example.com/’寫成‘http://example.com’錯誤
times.redirect = t.redirectEnd - redirectStart
// DNS解析時間 - 可增加DNS預加載。頁面涉及域名是否過多
times.lookupDomian = t.domainLookupEnd - t.domainLookupStart
// 首字節響應時間 - 數據鏈路的響應速度,受機房,CDN,帶寬,服務器性能等影響
times.ttfb = t.responseStart - t.navigationStart
// 資源加載完成時間 - Nginx上配置gzip壓縮減少下載資源
times.request = t.responseEnd - t.reuqestStart
// onload執行效率 - 避免過多邏輯在onload中執行,考慮資源懶加載,延遲獲取等
times.loadEvent = t.loadEventEnd - t.loadEventStart
// DNS緩存時間
times.appcache = t.domianLookupStart - t.fetchStart
// 卸載頁面時間
times.unloadEvent = t.unloadEventEnd - t.unloadEventStart
// TCP連接建立及完成握手時間
times.connect = t.connectEnd - t.connectStart
return times
}
?
# Performance.navigation 只讀
? .navigation
返回一個performanceNavigation對象,提供了在指定的時間段里發生的操作和相關信息,包括頁面是加載、刷新還是重定向。
const navigation = window.performance.navigation
? 該對象返回值信息如下
-
頁面載入類型 -
type
-
0
:同TYPE_NAVIGATE
;如點擊鏈接,url輸入,腳本執行跳轉,或書簽和表單的提交等方式載入 -
1
:同TYPE_RELOAD
;如點擊刷新頁面按鈕,或腳本Location.reload()
載入 -
2
:同TYPE_BACK_FORWARD
;通過歷史記錄的前進和后退進入 -
255
:同TYPE_RESERVED
;通過其他方式進入
-
- 重定向次數 -
redirectCount
-
序列化方法 -
toJson()
鏈式調用辦法,將PerformanceNavigation轉化為JSON對象。
?
$ Performance 方法
? timing
屬性主要針對文檔載入及之前的各個節點性能監控,無法落實到其他業務邏輯執行。想要監控更多信息,就需要使用Performance接口提供的方法來實現。
# now() (單位ms)
? performance.now()
方法返回了相對于 performance.timing.navigationStart
(頁面初始化) 的時間,而Date.now()
返回的是UNIX時間也就是距1970年的時間。且因為performance.now()
的時間是以一定速率慢慢增加的,不受系統時間影響,也不受進程阻塞影響,比Date.now()
時間來的更精準一些。
let t0 = window.performance.now();
todo()
let t1 = window.performance.now();
console.log("todo執行時間:", (t1 - t0) + "毫秒.")
# getEntries()
? 返回一個按startTime
排序的數組,包含加載本頁面所有的資源請求相關時間數據的集合。為更好的理解看一個entry實例數據,以訪問https://www.baidu.com/
為例:
const entries = window.performance.getEntries()
console.log(entries)
以下為返回數組的第一項:
? 可以發現,整個 Performance.timing 的數據節點均已包含。除此之外,還包括了以下幾個信息:
-
name:資源名稱。是資源的絕對路徑或
mark()
方法自定義的名稱。 - startTime:開始時間
- duration:加載時間
- entryType:資源類型;詳情如下
- initiatorType:請求發起者;詳情如下
entryType的值
值 | 描述 |
---|---|
mark |
通過mark()添加到數組中的對象 |
measure |
通過measure()添加到數組中的對象 |
resource |
所有資源加載時間(重要) |
navigation |
導航相關信息,僅chrome和Opera支持 |
frame |
- |
server |
- |
initiatorType的值
值 | 發起對象 | 描述 |
---|---|---|
link/script/img/iframe 等 |
某個標簽元素 | 標簽形式加載 |
css |
某個css樣式 | 通過css樣式加載,如background 的url() 資源 |
xmlhttprequest |
某個http請求 | 通過xhr加載的資源 |
navigation |
某個performanceNavigation對象 | 當對象是PerformanceNavigationTiming時返回 |
? 因此,我們獲取其性能時間數據可封裝函數如下
// 計算加載時間
function getEntryTiming (entry) {
var t = entry;
var times = {};
// 重定向的時間
times.redirect = t.redirectEnd - t.redirectStart;
// DNS 查詢時間
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
// 內容加載完成的時間
times.request = t.responseEnd - t.requestStart;
// TCP 建立連接完成握手的時間
times.connect = t.connectEnd - t.connectStart;
// 掛載 entry 返回
times.name = entry.name;
times.entryType = entry.entryType;
times.initiatorType = entry.initiatorType;
times.duration = entry.duration;
return times;
}
// run it
var entries = window.performance.getEntries();
entries.forEach(function (entry) {
var times = getEntryTiming(entry);
console.log(times);
});
? 執行該方法會發現,一個全量的entries
存在了過多的干擾信息,如果要從中挑出某些有用項進行比較只能通過數組過濾手段實現比較麻煩,好在performance接口提供了這個方法
# getEntriesByType()
? performance.getEntriesByType()
方法返回給定類型的entries
數組集合,其本質就是在全量數據中按entryType
屬性過濾,返回過濾后的數據,效果等同于Array.filter()
。該方法常配合mark()
方法使用,用來獲取用戶自己打的標簽數據。
entries = window.performance.getEntriesByType(type);
# getEntriesByName()
? 使用辦法同getEntriesByType()
,接受一個參數,用于指定entries
名稱??梢杂脕斫y計某一個函數被執行的次數及各個執行時刻,另一個更重要的是用來檢索measure測量的duration耗時。
?
# mark()
? 使用performance.mark()
也可以精準的計算程序的執行時間。思路就是在某些關鍵位置插入一些標記,當程序運行到標記處時,Performance會入棧一個entry
。這樣,通過在需要分析性能的邏輯段落前后插入不同的標記,來實現對該處性能的監控。
function markSample(name) {
const markStart = name + '_markStart'
const markEnd= name + '_markEnd'
window.performance.mark(markStart)
for(let i = 0; i < 100; i++) {
for(let j = 0; j < 100; j++) {
// TODO:
}
}
window.performance.mark(markEnd)
}
// run it
markSample(‘first’)
const marks = window.performance.getEntriesByType('mark')
console.log(marks)
執行結果會包含四個關鍵屬性,如下:
# measure()
? performance.measure()
用于測量兩個標記之間執行的時間,并把它賦值給第一個參數(measure名稱)上。如在上例的markSample
函數底部插入一下代碼
window.performance.measure('measure_test', markStart, markEnd)
var measureTest= window.performance.getEntriesByName('measure_test');
console.log(measureTest);
?
? 值得關注的是,由于標記在插入后,每次程序執行到此處將入棧一個entry
,而該數據是記錄在全局的window
下的,因此當標記過多或被執行次數太多時,可能出現內存污染等問題,因此,這就要求在標記使用結束后及時清除他們。
# clearMarks()
? performance.clearMarks()
接受 0/1 個參數,表示將要清除的標記名稱
// 指定清除某個標記
window.performance.clearMarks('first_markStart')
// 清除所有標記
window.performance.clearMarks()
# clearMeasures()
? 測量完成后也應當及時清除,用法:
// 清除指定測量
window.performance.clearMeasures('first_measure');
// 清除所有測量
window.performance.clearMeasures();
?
$ 使用mark測量timing事件
? 可能有個錯誤的理解就是performance.measure()
只能測量performance.mark()
的標記,其實不然,比如,在timing中,我們是這么測量domReady事件的:
cosnt t = performance.timing
const domReady = t.domComplete - t.responseEnd;
console.log(domReady )
也可以使用measure()
來實現如下:
window.performance.measure('domReady','responseEnd' , 'domComplete');
var domReadyMeasure = window.performance.getEntriesByName('domReady');
console.log(domReadyMeasure);
?
$ refs
參考文獻
performance - MDN
HTML5 performance API 草案.
初探performance - AlloyTeam