初探 performance – 監控網頁與程序性能
使用 window.performance 提供了一組精確的數據,經過簡單的計算就能得出一些網頁性能數據。
配合上報一些客戶端瀏覽器的設備類型等數據,就可以實現簡單的統計啦!
額,先看下兼容性如何: http://caniuse.com/#feat=nav-timing
這篇文章中 Demo 的運行環境為最新的 Chrome 的控制臺,如果你用的是其他瀏覽器,自查兼容性哈~
先來看看在 Chrome 瀏覽器控制臺中執行 window.performance 會出現什么:
簡單解釋下 performance 中的屬性:
先看下一個請求發出的整個過程中,各種環節的時間順序:
// 獲取 performance 數據 var performance = { // memory 是非標準屬性,只在 Chrome 有 // 財富問題:我有多少內存 memory : { usedJSHeapSize : 16100000 , // JS 對象(包括V8引擎內部對象)占用的內存,一定小于 totalJSHeapSize totalJSHeapSize : 35100000 , // 可使用的內存 jsHeapSizeLimit : 793000000 // 內存大小限制 } , // 哲學問題:我從哪里來? navigation : { redirectCount : 0 , // 如果有重定向的話,頁面通過幾次重定向跳轉而來 type : 0 // 0 即 TYPE_NAVIGATENEXT 正常進入的頁面(非刷新、非重定向等) // 1 即 TYPE_RELOAD 通過 window.location.reload() 刷新的頁面 // 2 即 TYPE_BACK_FORWARD 通過瀏覽器的前進后退按鈕進入的頁面(歷史記錄) // 255 即 TYPE_UNDEFINED 非以上方式進入的頁面 } , timing : { // 在同一個瀏覽器上下文中,前一個網頁(與當前頁面不一定同域)unload 的時間戳,如果無前一個網頁 unload ,則與 fetchStart 值相等 navigationStart : 1441112691935 , // 前一個網頁(與當前頁面同域)unload 的時間戳,如果無前一個網頁 unload 或者前一個網頁與當前頁面不同域,則值為 0 unloadEventStart : 0 , // 和 unloadEventStart 相對應,返回前一個網頁 unload 事件綁定的回調函數執行完畢的時間戳 unloadEventEnd : 0 , // 第一個 HTTP 重定向發生時的時間。有跳轉且是同域名內的重定向才算,否則值為 0 redirectStart : 0 , // 最后一個 HTTP 重定向完成時的時間。有跳轉且是同域名內部的重定向才算,否則值為 0 redirectEnd : 0 , // 瀏覽器準備好使用 HTTP 請求抓取文檔的時間,這發生在檢查本地緩存之前 fetchStart : 1441112692155 , // DNS 域名查詢開始的時間,如果使用了本地緩存(即無 DNS 查詢)或持久連接,則與 fetchStart 值相等 domainLookupStart : 1441112692155 , // DNS 域名查詢完成的時間,如果使用了本地緩存(即無 DNS 查詢)或持久連接,則與 fetchStart 值相等 domainLookupEnd : 1441112692155 , // HTTP(TCP) 開始建立連接的時間,如果是持久連接,則與 fetchStart 值相等 // 注意如果在傳輸層發生了錯誤且重新建立連接,則這里顯示的是新建立的連接開始的時間 connectStart : 1441112692155 , // HTTP(TCP) 完成建立連接的時間(完成握手),如果是持久連接,則與 fetchStart 值相等 // 注意如果在傳輸層發生了錯誤且重新建立連接,則這里顯示的是新建立的連接完成的時間 // 注意這里握手結束,包括安全連接建立完成、SOCKS 授權通過 connectEnd : 1441112692155 , // HTTPS 連接開始的時間,如果不是安全連接,則值為 0 secureConnectionStart : 0 , // HTTP 請求讀取真實文檔開始的時間(完成建立連接),包括從本地讀取緩存 // 連接錯誤重連時,這里顯示的也是新建立連接的時間 requestStart : 1441112692158 , // HTTP 開始接收響應的時間(獲取到第一個字節),包括從本地讀取緩存 responseStart : 1441112692686 , // HTTP 響應全部接收完成的時間(獲取到最后一個字節),包括從本地讀取緩存 responseEnd : 1441112692687 , // 開始解析渲染 DOM 樹的時間,此時 Document.readyState 變為 loading,并將拋出 readystatechange 相關事件 domLoading : 1441112692690 , // 完成解析 DOM 樹的時間,Document.readyState 變為 interactive,并將拋出 readystatechange 相關事件 // 注意只是 DOM 樹解析完成,這時候并沒有開始加載網頁內的資源 domInteractive : 1441112693093 , // DOM 解析完成后,網頁內資源加載開始的時間 // 在 DOMContentLoaded 事件拋出前發生 domContentLoadedEventStart : 1441112693093 , // DOM 解析完成后,網頁內資源加載完成的時間(如 JS 腳本加載執行完畢) domContentLoadedEventEnd : 1441112693101 , // DOM 樹解析完成,且資源也準備就緒的時間,Document.readyState 變為 complete,并將拋出 readystatechange 相關事件 domComplete : 1441112693214 , // load 事件發送給文檔,也即 load 回調函數開始執行的時間 // 注意如果沒有綁定 load 事件,值為 0 loadEventStart : 1441112693214 , // load 事件的回調函數執行完畢的時間 loadEventEnd : 1441112693215 // 字母順序 // connectEnd: 1441112692155, // connectStart: 1441112692155, // domComplete: 1441112693214, // domContentLoadedEventEnd: 1441112693101, // domContentLoadedEventStart: 1441112693093, // domInteractive: 1441112693093, // domLoading: 1441112692690, // domainLookupEnd: 1441112692155, // domainLookupStart: 1441112692155, // fetchStart: 1441112692155, // loadEventEnd: 1441112693215, // loadEventStart: 1441112693214, // navigationStart: 1441112691935, // redirectEnd: 0, // redirectStart: 0, // requestStart: 1441112692158, // responseEnd: 1441112692687, // responseStart: 1441112692686, // secureConnectionStart: 0, // unloadEventEnd: 0, // unloadEventStart: 0 } } ; |
具體的含義都在注釋里說明了,接下來我們看下能用這些數據做什么?
使用 performance.timing 信息簡單計算出網頁性能數據
在注釋中,我用【重要】標注了我個人認為比較有用的數據,用【原因】標注了為啥要重點關注這個數據
// 計算加載時間 function getPerformanceTiming ( ) { var performance = window . performance ; if ( ! performance ) { // 當前瀏覽器不支持 console . log ( '你的瀏覽器不支持 performance 接口' ) ; return ; } var t = performance . timing ; var times = { } ; //【重要】頁面加載完成的時間 //【原因】這幾乎代表了用戶等待頁面可用的時間 times . loadPage = t . loadEventEnd - t . navigationStart ; //【重要】解析 DOM 樹結構的時間 //【原因】反省下你的 DOM 樹嵌套是不是太多了! times . domReady = t . domComplete - t . responseEnd ; //【重要】重定向的時間 //【原因】拒絕重定向!比如,http://example.com/ 就不該寫成 http://example.com times . redirect = t . redirectEnd - t . redirectStart ; //【重要】DNS 查詢時間 //【原因】DNS 預加載做了么?頁面內是不是使用了太多不同的域名導致域名查詢的時間太長? // 可使用 HTML5 Prefetch 預查詢 DNS ,見:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364) times . lookupDomain = t . domainLookupEnd - t . domainLookupStart ; //【重要】讀取頁面第一個字節的時間 //【原因】這可以理解為用戶拿到你的資源占用的時間,加異地機房了么,加CDN 處理了么?加帶寬了么?加 CPU 運算速度了么? // TTFB 即 Time To First Byte 的意思 // 維基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte times . ttfb = t . responseStart - t . navigationStart ; //【重要】內容加載完成的時間 //【原因】頁面內容經過 gzip 壓縮了么,靜態資源 css/js 等壓縮了么? times . request = t . responseEnd - t . requestStart ; //【重要】執行 onload 回調函數的時間 //【原因】是否太多不必要的操作都放到 onload 回調函數里執行了,考慮過延遲加載、按需加載的策略么? times . loadEvent = t . loadEventEnd - t . loadEventStart ; // DNS 緩存時間 times . appcache = t . domainLookupStart - t . fetchStart ; // 卸載頁面的時間 times . unloadEvent = t . unloadEventEnd - t . unloadEventStart ; // TCP 建立連接完成握手的時間 times . connect = t . connectEnd - t . connectStart ; return times ; } |
使用performance.getEntries() 獲取所有資源請求的時間數據
這個函數返回的將是一個數組,包含了頁面中所有的 HTTP 請求,這里拿第一個請求 window.performance.getEntries()[0] 舉例。 注意 HTTP 請求有可能命中本地緩存,所以請求響應的間隔將非常短 可以看到,與 performance.timing 對比: 沒有與 DOM 相關的屬性:
-
navigationStart
-
unloadEventStart
-
unloadEventEnd
-
domLoading
-
domInteractive
-
domContentLoadedEventStart
-
domContentLoadedEventEnd
-
domComplete
-
loadEventStart
-
loadEventEnd
新增屬性:
-
name
-
entryType
-
initiatorType
-
duration
與 window.performance.timing 中包含的屬性就不再介紹了:
var entry = { // 資源名稱,也是資源的絕對路徑 name : "http://cdn.alloyteam.com/wp-content/themes/alloyteam/style.css" , // 資源類型 entryType : "resource" , // 誰發起的請求 initiatorType : "link" , // link 即 <link> 標簽 // script 即 <script> // redirect 即重定向 // 加載時間 duration : 18.13399999809917 , redirectStart : 0 , redirectEnd : 0 , fetchStart : 424.57699999795295 , domainLookupStart : 0 , domainLookupEnd : 0 , connectStart : 0 , connectEnd : 0 , secureConnectionStart : 0 , requestStart : 0 , responseStart : 0 , responseEnd : 442.7109999960521 , startTime : 424.57699999795295 } ; |
可以像 getPerformanceTiming 獲取網頁的時間一樣,獲取某個資源的時間:
// 計算加載時間 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 ; } // test // var entries = window.performance.getEntries(); // entries.forEach(function (entry) { // var times = getEntryTiming(entry); // console.log(times); // }); |
使用 performance.now() 精確計算程序執行時間
performance.now() 與 Date.now() 不同的是,返回了以微秒(百萬分之一秒)為單位的時間,更加精準。
并且與 Date.now() 會受系統程序執行阻塞的影響不同,performance.now() 的時間是以恒定速率遞增的,不受系統時間的影響(系統時間可被人為或軟件調整)。
注意 Date.now() 輸出的是 UNIX 時間,即距離 1970 的時間,而 performance.now() 輸出的是相對于 performance.timing.navigationStart(頁面初始化) 的時間。
使用 Date.now() 的差值并非絕對精確,因為計算時間時受系統限制(可能阻塞)。但使用 performance.now() 的差值,并不影響我們計算程序執行的精確時間。
// 計算程序執行的精確時間 function getFunctionTimeWithDate ( func ) { var timeStart = Data . now ( ) ; // 執行開始 func ( ) ; // 執行結束 var timeEnd = Data . now ( ) ; // 返回執行時間 return ( timeEnd - timeStart ) ; } function getFunctionTimeWithPerformance ( func ) { var timeStart = window . performance . now ( ) ; // 執行開始 func ( ) ; // 執行結束 var timeEnd = window . performance . now ( ) ; // 返回執行時間 return ( timeEnd - timeStart ) ; } |
使用 performance.mark() 也可以精確計算程序執行時間
使用 performance.mark() 標記各種時間戳(就像在地圖上打點),保存為各種測量值(測量地圖上的點之間的距離),便可以批量地分析這些數據了。
直接上示例代碼看注釋便明白:
function randomFunc ( n ) { if ( ! n ) { // 生成一個隨機數 n = ~ ~ ( Math . random ( ) * 10000 ) ; } var nameStart = 'markStart' + n ; var nameEnd = 'markEnd' + n ; // 函數執行前做個標記 window . performance . mark ( nameStart ) ; for ( var i = 0 ; i < n ; i ++ ) { // do nothing } // 函數執行后再做個標記 window . performance . mark ( nameEnd ) ; // 然后測量這個兩個標記間的時間距離,并保存起來 var name = 'measureRandomFunc' + n ; window . performance . measure ( name , nameStart , nameEnd ) ; } // 執行三次看看 randomFunc ( ) ; randomFunc ( ) ; // 指定一個名字 randomFunc ( 888 ) ; |
// 看下保存起來的標記 mark var marks = window . performance . getEntriesByType ( 'mark' ) ; console . log ( marks ) ; |
// 看下保存起來的測量 measure var measure = window . performance . getEntriesByType ( 'measure' ) ; console . log ( measure ) ; |
// 看下我們自定義的測量 var entries = window . performance . getEntriesByName ( 'measureRandomFunc888' ) ; console . log ( entries ) ; |
可以看到,for 循環 measureRandomFunc888 的時候
結束時間為: 4875.1199999969685
開始時間為:4875.112999987323
執行時間為:4875.1199999969685 – 4875.112999987323 = 0.00700000964
標記和測量用完了可以清除掉:
// 清除指定標記 window . performance . clearMarks ( 'markStart888' ) ; // 清除所有標記 window . performance . clearMarks ( ) ; // 清除指定測量 window . performance . clearMeasures ( 'measureRandomFunc' ) ; // 清除所有測量 window . performance . clearMeasures ( ) ; |
當然 performance.mark() 只是提供了一些簡便的測量方式,比如之前我們測量 domReady 是這么測的:
// 計算 domReady 時間 var t = performance . timing var domReadyTime = t . domComplete - t . responseEnd ; console . log ( domReadyTime ) |
其實就可以寫成:
window . performance . measure ( 'domReady' , 'responseEnd' , 'domComplete' ) ; var domReadyMeasure = window . performance . getEntriesByName ( 'domReady' ) ; console . log ( domReadyMeasure ) ; |
拋磚引玉:performance 數據能干啥用?
熟悉 Chrome 開發者工具的朋友應該知道:在開發環境下,其實我們自己打開 Chrome 的開發者工具,切換到網絡面板,就能很詳細的看到網頁性能相關的數據。但當我們需要統計分析用戶打開我們網頁時的性能如何時,我們將 performance 原始信息或通過簡單計算后的信息(如上面寫到的 getPerformanceTiming() 和 getEntryTiming()) 上傳到服務器,配合其他信息(如 HTTP 請求頭信息),就完美啦~