H5性能監控「Performance」

? 性能監控 在前端一直是一個口頭上備受關注但開發中又常被忽略的點,畢竟不是每個開發者很容易就做到的事。好在HTML5新增了performance特性,它是High Resolution Time API 的一部分,目的在于獲取到當前頁面中與性能相關的信息,以便幫助開發者直觀感受頁面性能及針對問題優化。
? 了解如何監控頁面性能前,我們先回顧幾個指標:
(1)白屏時間:頁面被打開,到首字節渲染呈現所需的時間。
(2)首屏時間:首屏內容渲染完成所需的時間。
(3)下載時間(HTTP請求耗時):頁面所需資源從服務器上下載完成所需的時間。
(4)DOM樹解析時間:資源下載完成到頁面構建展示出來所需的時間。
? ...
? 這些信息都如何獲取?在此標準之前,也有一些手段可以實現,但H5的performance直接來源于瀏覽器,與手工Date.time,Cookie等對比,使用上更方便,數據上更準確。(Date.now()會受程序阻塞影響)
?


$ Performance 屬性

? 關于performance屬性,建議讀者自己在工具編輯器上直接打印出來看看更能真切的體會該接口。本文主要介紹前兩者,對其他內容感興趣的同學,可以 戳這里

  1. .timing(只讀):對象;包含了延遲相關的性能信息。
  2. .navigation(只讀):對象;包含了指定的時間段里發生的操作相關信息,包括頁面是加載還是刷新、發生了多少次重定向等等。
  3. .timeOrigin(只讀):即將失效。用于返回性能測量開始時的高精度時間戳。
  4. .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樣式加載,如backgroundurl()資源
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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容