? 性能監(jiān)控 在前端一直是一個口頭上備受關(guān)注但開發(fā)中又常被忽略的點(diǎn),畢竟不是每個開發(fā)者很容易就做到的事。好在HTML5新增了performance特性,它是High Resolution Time API 的一部分,目的在于獲取到當(dāng)前頁面中與性能相關(guān)的信息,以便幫助開發(fā)者直觀感受頁面性能及針對問題優(yōu)化。
? 了解如何監(jiān)控頁面性能前,我們先回顧幾個指標(biāo):
(1)白屏?xí)r間:頁面被打開,到首字節(jié)渲染呈現(xiàn)所需的時間。
(2)首屏?xí)r間:首屏內(nèi)容渲染完成所需的時間。
(3)下載時間(HTTP請求耗時):頁面所需資源從服務(wù)器上下載完成所需的時間。
(4)DOM樹解析時間:資源下載完成到頁面構(gòu)建展示出來所需的時間。
? ...
? 這些信息都如何獲???在此標(biāo)準(zhǔn)之前,也有一些手段可以實(shí)現(xiàn),但H5的performance直接來源于瀏覽器,與手工Date.time,Cookie等對比,使用上更方便,數(shù)據(jù)上更準(zhǔn)確。(Date.now()會受程序阻塞影響)
?
$ Performance 屬性
? 關(guān)于performance屬性,建議讀者自己在工具編輯器上直接打印出來看看更能真切的體會該接口。本文主要介紹前兩者,對其他內(nèi)容感興趣的同學(xué),可以 戳這里
-
.timing
(只讀):對象;包含了延遲相關(guān)的性能信息。 -
.navigation
(只讀):對象;包含了指定的時間段里發(fā)生的操作相關(guān)信息,包括頁面是加載還是刷新、發(fā)生了多少次重定向等等。 -
.timeOrigin
(只讀):即將失效。用于返回性能測量開始時的高精度時間戳。 -
.memory:由chrome拓展的非標(biāo)準(zhǔn)屬性,用于返回基本內(nèi)存的使用情況。注意非chrome不支持。
?
# Performance.timing 只讀
const PerformanceTiming = window.performance.timing
? 返回值為一個對象,記錄著完整的頁面加載信息。其各個節(jié)點(diǎn)如下:

? 看著上圖,回顧一下一般意義的頁面加載過程:瀏覽器向服務(wù)器請求資源 --> DOM結(jié)構(gòu)解析 --> 構(gòu)建DOM樹 --> 構(gòu)建CSS規(guī)則樹 --> 構(gòu)建渲染樹 --> 繪制頁面??梢钥闯觯@個過程只是上圖中的某一小部分,我們來詳談一下實(shí)際的整個過程
-
Prompt for unload 階段
-
.navigationStart:瀏覽器完成卸載前一個文檔的時間。如果沒前一個文檔,則該值與第三步.fetchStart的值相同。 -
.unloadEventStart:返回前一個同源文檔出發(fā)卸載(unload)事件前的時間。如果沒有前一個文檔,或前文檔與本文檔不同源,或需重定向,則返回0。 -
.unloadEventEnd:返回前一個同源文檔完成卸載的時間。如果沒有或文檔不同源,則返回0.
-
-
Redirect 階段
-
.redirectStart:http重定向開始的時間。如果中間有多個重定向,且每個重定向均同源,則返回第一個重定向的.fetchStart時間,若不同源,則為0 -
.redirectEnd:http重定向結(jié)束時間。如果中間有多個重定向且均同源,則返回最后一個重定向結(jié)束時間。若不同源,則為0。
-
-
App cache 階段
-
.fetchStart:瀏覽器準(zhǔn)備好使用HTTP請求來獲取(fetch)文檔的時間,這個時間會在檢查任何應(yīng)用緩存之前。
-
-
DNS查詢階段
-
.domainLookupStart:用戶代理對當(dāng)前文檔所屬域進(jìn)行DNS查詢開始的時間。如果是長連接(如websocket),或本地緩存了,則該值與.fetchStart相同 -
.domainLookupEnd:域名查詢結(jié)束的時間。如果是長連接,或本地緩存了,則該值與.fetchStart相同
-
-
TCP連接階段
-
.connectStart:用戶代理開始向服務(wù)器請求所需文檔時,連接建立的開始時間。如果是長連接,或本地緩存了,則該值與.fetchStart相同 -
.secureConnectStart:返回與服務(wù)器開始SSL握手時的時間。異常情況同上。 -
.connectEnd: HTTP握手成功,認(rèn)證結(jié)束,連接建立時的時間。如果是長連接,或本地緩存了,則該值與.fetchStart相同。
-
-
Request 階段
-
requestStart:從服務(wù)器/緩存/本地資源中開始請求文檔的時間。如果連接發(fā)生斷開重連,該信息會被刷新。 - 沒有請求結(jié)束時間是因?yàn)樵搫幼靼l(fā)生在服務(wù)器端,且受數(shù)據(jù)鏈路等各個因素影響,瀏覽器并不能準(zhǔn)確反饋該信息
-
-
Response 階段
-
.responseStart:從服務(wù)器/緩存/本地資源中接收到第一個字節(jié)時的時間。如果連接發(fā)生斷開重連,該信息會被刷新。 -
.responseEnd:從服務(wù)器/緩存/本地資源中接收到最后一個字節(jié)時的時間。如果連接提前關(guān)閉,則返回提前關(guān)閉的時間。獲取該值時需注意要在Response結(jié)束之后,如window.onload,否則可能不準(zhǔn)確。
-
-
Processing 執(zhí)行階段
-
.domLoading:資源下載完成,開始解析DOM結(jié)構(gòu),當(dāng)Document.readyState的值更新為loading時的時間。 -
.domInteractive:DOM解析完成,開始加載內(nèi)嵌資源,即Document.readyState的值更新為interactive時的時間 - 執(zhí)行階段內(nèi)的 DOMContentLoaded 階段
-
.domContentLoadedEventStart:解析器發(fā)送DOMContentLoaded事件,所有需要被執(zhí)行的腳本均解析完成時的時間。 -
.domContentLoadedEventEnd:所有立即執(zhí)行的腳本均執(zhí)行完成時的時間。不執(zhí)行的腳本如懶加載資源不在該范圍內(nèi)。
-
-
.domComplete:當(dāng)前文檔解析完成,document.readyState的值更新為complete時的時間。
-
-
load 業(yè)務(wù)涉入階段
-
.loadEventStart:文檔觸發(fā)load事件的時間,如果還沒觸發(fā),則返回0。 -
.loadEventEnd:文檔結(jié)束load事件的時間,未觸發(fā)則返回0。
?
-
# 性能監(jiān)控指標(biāo)
? 通過以上的各個事件分析,不難得出如下各個時間段:
const timing = window.performance.timing
-
DNS解析耗時:
timing.domainLookupEnd - timing.domainLookupStart -
TCP連接耗時:
timing.connectEnd - timing.connectStart -
發(fā)送請求耗時:
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
? 除此之外,在文首提到的其他幾個性能指標(biāo),如下:
-
白屏?xí)r間:
timing.responseStart - timing.navigationStart -
首屏?xí)r間:
timing.domComplete- timing.navigationStart -
資源下載總耗時:
timing.responseEnd - timing.requestStart; -
請求完畢至DOM加載:
timing.domInteractive - timing.responseEnd
?
# 實(shí)戰(zhàn)案例
? 封裝一個函數(shù)如下,注釋前半部為參數(shù)功能,后半部為監(jiān)控到頁面性能問題時可能的原因
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樹結(jié)構(gòu)時間 - DOM樹嵌套不宜太深
times.domReady = t.domeComplete - t.responseEnd
// 重定向時間 - 若拒絕重定向,檢查是否有類似‘http://example.com/’寫成‘http://example.com’錯誤
times.redirect = t.redirectEnd - redirectStart
// DNS解析時間 - 可增加DNS預(yù)加載。頁面涉及域名是否過多
times.lookupDomian = t.domainLookupEnd - t.domainLookupStart
// 首字節(jié)響應(yīng)時間 - 數(shù)據(jù)鏈路的響應(yīng)速度,受機(jī)房,CDN,帶寬,服務(wù)器性能等影響
times.ttfb = t.responseStart - t.navigationStart
// 資源加載完成時間 - Nginx上配置gzip壓縮減少下載資源
times.request = t.responseEnd - t.reuqestStart
// onload執(zhí)行效率 - 避免過多邏輯在onload中執(zhí)行,考慮資源懶加載,延遲獲取等
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對象,提供了在指定的時間段里發(fā)生的操作和相關(guān)信息,包括頁面是加載、刷新還是重定向。
const navigation = window.performance.navigation
? 該對象返回值信息如下

-
頁面載入類型 -
type-
0:同TYPE_NAVIGATE;如點(diǎn)擊鏈接,url輸入,腳本執(zhí)行跳轉(zhuǎn),或書簽和表單的提交等方式載入 -
1:同TYPE_RELOAD;如點(diǎn)擊刷新頁面按鈕,或腳本Location.reload()載入 -
2:同TYPE_BACK_FORWARD;通過歷史記錄的前進(jìn)和后退進(jìn)入 -
255:同TYPE_RESERVED;通過其他方式進(jìn)入
-
- 重定向次數(shù) -
redirectCount -
序列化方法 -
toJson()
鏈?zhǔn)秸{(diào)用辦法,將PerformanceNavigation轉(zhuǎn)化為JSON對象。
?
$ Performance 方法
? timing屬性主要針對文檔載入及之前的各個節(jié)點(diǎn)性能監(jiān)控,無法落實(shí)到其他業(yè)務(wù)邏輯執(zhí)行。想要監(jiān)控更多信息,就需要使用Performance接口提供的方法來實(shí)現(xiàn)。
# now() (單位ms)
? performance.now()方法返回了相對于 performance.timing.navigationStart(頁面初始化) 的時間,而Date.now()返回的是UNIX時間也就是距1970年的時間。且因?yàn)?code>performance.now()的時間是以一定速率慢慢增加的,不受系統(tǒng)時間影響,也不受進(jìn)程阻塞影響,比Date.now()時間來的更精準(zhǔn)一些。
let t0 = window.performance.now();
todo()
let t1 = window.performance.now();
console.log("todo執(zhí)行時間:", (t1 - t0) + "毫秒.")
# getEntries()
? 返回一個按startTime排序的數(shù)組,包含加載本頁面所有的資源請求相關(guān)時間數(shù)據(jù)的集合。為更好的理解看一個entry實(shí)例數(shù)據(jù),以訪問https://www.baidu.com/為例:
const entries = window.performance.getEntries()
console.log(entries)
以下為返回數(shù)組的第一項(xiàng):

? 可以發(fā)現(xiàn),整個 Performance.timing 的數(shù)據(jù)節(jié)點(diǎn)均已包含。除此之外,還包括了以下幾個信息:
-
name:資源名稱。是資源的絕對路徑或
mark()方法自定義的名稱。 - startTime:開始時間
- duration:加載時間
- entryType:資源類型;詳情如下
- initiatorType:請求發(fā)起者;詳情如下
entryType的值
| 值 | 描述 |
|---|---|
mark |
通過mark()添加到數(shù)組中的對象 |
measure |
通過measure()添加到數(shù)組中的對象 |
resource |
所有資源加載時間(重要) |
navigation |
導(dǎo)航相關(guān)信息,僅chrome和Opera支持 |
frame |
- |
server |
- |
initiatorType的值
| 值 | 發(fā)起對象 | 描述 |
|---|---|---|
link/script/img/iframe等 |
某個標(biāo)簽元素 | 標(biāo)簽形式加載 |
css |
某個css樣式 | 通過css樣式加載,如background的url()資源 |
xmlhttprequest |
某個http請求 | 通過xhr加載的資源 |
navigation |
某個performanceNavigation對象 | 當(dāng)對象是PerformanceNavigationTiming時返回 |
? 因此,我們獲取其性能時間數(shù)據(jù)可封裝函數(shù)如下
// 計算加載時間
function getEntryTiming (entry) {
var t = entry;
var times = {};
// 重定向的時間
times.redirect = t.redirectEnd - t.redirectStart;
// DNS 查詢時間
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
// 內(nèi)容加載完成的時間
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);
});
? 執(zhí)行該方法會發(fā)現(xiàn),一個全量的entries存在了過多的干擾信息,如果要從中挑出某些有用項(xiàng)進(jìn)行比較只能通過數(shù)組過濾手段實(shí)現(xiàn)比較麻煩,好在performance接口提供了這個方法
# getEntriesByType()
? performance.getEntriesByType()方法返回給定類型的entries數(shù)組集合,其本質(zhì)就是在全量數(shù)據(jù)中按entryType屬性過濾,返回過濾后的數(shù)據(jù),效果等同于Array.filter()。該方法常配合mark()方法使用,用來獲取用戶自己打的標(biāo)簽數(shù)據(jù)。
entries = window.performance.getEntriesByType(type);
# getEntriesByName()
? 使用辦法同getEntriesByType(),接受一個參數(shù),用于指定entries名稱??梢杂脕斫y(tǒng)計某一個函數(shù)被執(zhí)行的次數(shù)及各個執(zhí)行時刻,另一個更重要的是用來檢索measure測量的duration耗時。
?
# mark()
? 使用performance.mark()也可以精準(zhǔn)的計算程序的執(zhí)行時間。思路就是在某些關(guān)鍵位置插入一些標(biāo)記,當(dāng)程序運(yùn)行到標(biāo)記處時,Performance會入棧一個entry。這樣,通過在需要分析性能的邏輯段落前后插入不同的標(biāo)記,來實(shí)現(xiàn)對該處性能的監(jiān)控。
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)
執(zhí)行結(jié)果會包含四個關(guān)鍵屬性,如下:

# measure()
? performance.measure()用于測量兩個標(biāo)記之間執(zhí)行的時間,并把它賦值給第一個參數(shù)(measure名稱)上。如在上例的markSample函數(shù)底部插入一下代碼
window.performance.measure('measure_test', markStart, markEnd)
var measureTest= window.performance.getEntriesByName('measure_test');
console.log(measureTest);
?
? 值得關(guān)注的是,由于標(biāo)記在插入后,每次程序執(zhí)行到此處將入棧一個entry,而該數(shù)據(jù)是記錄在全局的window下的,因此當(dāng)標(biāo)記過多或被執(zhí)行次數(shù)太多時,可能出現(xiàn)內(nèi)存污染等問題,因此,這就要求在標(biāo)記使用結(jié)束后及時清除他們。
# clearMarks()
? performance.clearMarks()接受 0/1 個參數(shù),表示將要清除的標(biāo)記名稱
// 指定清除某個標(biāo)記
window.performance.clearMarks('first_markStart')
// 清除所有標(biāo)記
window.performance.clearMarks()
# clearMeasures()
? 測量完成后也應(yīng)當(dāng)及時清除,用法:
// 清除指定測量
window.performance.clearMeasures('first_measure');
// 清除所有測量
window.performance.clearMeasures();
?
$ 使用mark測量timing事件
? 可能有個錯誤的理解就是performance.measure()只能測量performance.mark()的標(biāo)記,其實(shí)不然,比如,在timing中,我們是這么測量domReady事件的:
cosnt t = performance.timing
const domReady = t.domComplete - t.responseEnd;
console.log(domReady )
也可以使用measure()來實(shí)現(xiàn)如下:
window.performance.measure('domReady','responseEnd' , 'domComplete');
var domReadyMeasure = window.performance.getEntriesByName('domReady');
console.log(domReadyMeasure);
?
$ refs
參考文獻(xiàn)
performance - MDN
HTML5 performance API 草案.
初探performance - AlloyTeam