編者按:本文做者是來自360奇舞團的前端開發工程師劉宇晨,同時也是 W3C 性能工做組成員。前端
上一回,筆者介紹了 Navigation Timing
和 Resource Timing
在監控頁面加載上的實際應用。git
這一回,筆者將帶領你們學習 Performance Timeline
和 User Timing
標準,並使用相應的 API,給前端代碼「跑個分」。github
真實業務中,時而會出現比較消耗性能的操做,特別是頻繁操做 DOM 的行爲。那麼如何量化這些操做的性能表現呢?瀏覽器
常見的作法,就是經過分別記錄函數執行前和執行以後的 Date.now()
,而後求差,得出具體的執行時間。緩存
記錄一兩個函數還好,多了的話,還須要開發者維護一個全局的 hash ,用來統計所有數據。服務器
隨着 Performance Timeline
+ User Timing
標準的推出,開發者能夠直接使用相應的 API,瀏覽器便會直接統計相關信息,從而顯著簡化了衡量前端性能的流程。markdown
Performance Timeline
?根據 W3C 的定義,Performance Timeline
旨在幫助 Web 開發者在 Web 應用的整個生命週期中訪問、檢測、獲取各種性能指標,並定義相關接口。網絡
User Timing
?User Timing
相較於 Performance Timeline
而言,更爲細節。該標準拓展了原有的 Performance
接口,並添加了供前端開發者主動記錄性能指標的新方法。前端工程師
截至到 2018 年 7 月 29 日,Performance Timeline
和 User Timing
的最新標準均爲 Level 2
,且均處於編輯草稿狀態。異步
圖爲 Performance Timeline Level 2
中 PerformanceObserver
API 的支持狀況:
Performance Timeline Level 2
在實際應用時,主要使用PerformanceObserver
API。
圖爲 User Timing
的支持狀況:
假如你有一段比較耗性能的函數 foo
,你好奇在不一樣瀏覽器中,執行 foo
所需的時間分別是多少,那麼你能夠這麼作:
const prefix = fix => input => `${fix}${input}` const prefixStart = prefix('start') const prefixEnd = prefix('end') const measure = (fn, name = fn.name) => { performance.mark(prefixStart(name)) fn() performance.mark(prefixEnd(name)) } 複製代碼
上述代碼中,使用了一個新的 API :performance.mark
。
根據標準,調用 performance.mark(markName)
時,發生了以下幾步:
PerformanceMark
對象(如下稱爲條目);name
屬性設置爲 markName
;entryType
屬性設置爲 'mark'
;startTime
屬性設置爲 performance.now()
的值;duration
屬性設置爲 0
;performance entry buffer
中;undefined
關於「放入隊列」的含義,請參見 w3c.github.io/performance… [1] 中 Queue a
PerformanceEntry
上述過程,能夠簡單理解爲,「請瀏覽器記錄一條名爲 markName
的性能記錄」。
以後,咱們能夠經過 performance.getEntriesByType
獲取具體數據:
const getMarks = key => { return performance .getEntriesByType('mark') // 只獲取經過 performance.mark 記錄的數據 .filter(({ name }) => name === prefixStart(key) || name === prefixEnd(key)) } const getDuration = entries => { const { start = 0, end = 0 } = entries.reduce((last, { name, startTime }) => { if (/^start/.test(name)) { last.start = startTime } else if (/^end/.test(name)) { last.end = startTime } return last }) return end - start } const retrieveResult = key => getDuration(getMarks(key)) 複製代碼
performance.getEntriesByType('mark')
就是指明獲取由 mark
產生的數據。
「獲取個數據,怎麼代碼還要一大坨?尤爲是
getDuration
中,區分開始、結束時間的部分,太瑣碎吧!?「
W3C 性能小組早就料到有人會抱怨,因而進一步設計了 performance.measure
方法~
performance.measure
方法接收三個參數,依次是 measureName
,startMark
以及 endMark
。
startMark
和 endMark
很容易理解,就是對應開始和結束時的 markName
。measureName
則是爲每個 measure
行爲,提供一個標識。
調用後,performance.measure
會根據 startMark
和 endMark
對應的兩條記錄(均由 performance.mark
產生),造成一條 entryType
爲 'measure'
的新記錄,並自動計算運行時長。
幕後發生的具體步驟,和 performance.mark
很相似,有興趣的讀者能夠參考規範中的 3.1.3 小節 www.w3.org/TR/user-tim… [2]。
使用 performance.measure
重構一下前的代碼:
const measure = (fn, name = fn.name) => { const startName = prefixStart(name) const endName = prefixEnd(name) performance.mark(startName) fn() performance.mark(endName) // 調用 measure performance.measure(name, startName, endName) } const getDuration = entries => { // 直接獲取 duration const [{ duration }] = entries return duration } const retrieveResult = key => getDuration(performance.getEntriesByName(key)) // 使用時 function foo() { // some code } measure(foo) const duration = retrieveResult('foo') console.log('duration of foo is:', duration) 複製代碼
如何?是否是更清晰、簡練了~
這裏,咱們直接經過
performance.getEntriesByName(measureName)
的形式,獲取由measure
產生的數據。
異步函數?async
await
來一套:
const asyncMeasure = async (fn, name = fn.name) => { const startName = prefixStart(name) const endName = prefixEnd(name) performance.mark(startName) await fn() performance.mark(endName) // 調用 measure performance.measure(name, startName, endName) } 複製代碼
mark |
measure |
|
---|---|---|
做用 | 進行某個操做時,記錄一個時間戳 | 針對起始 + 結束的 mark 值,彙總造成一個直接可用的性能數據 |
不足 | 對於一個操做,須要兩個時間戳才能衡量性能表現 | 想要測量多個操做時,須要重複調用 |
以上相關 API,所有來自於 User Timing Level 2
。當加入 Performance Timeline
後,咱們能夠進一步優化代碼結構~
如上文所述,每次想看性能表現,彷佛都要主動調用一次 retrieveResult
函數。一兩次還好,次數多了,無疑增長了重複代碼,違反了 DRY 的原則。
在 Performance Timeline Level 2
中,工做組添加了新的 PerformanceObserver
接口,旨在解決如下三個問題:
performance buffer
時,產生資源競爭狀況。對於前端工程師而言,實際使用時只是換了一套 API 。
依舊是測量某操做的性能表現,在支持 Performance Timeline Level 2
的瀏覽器中,能夠這麼寫:
const observer = new PerformanceObserver(list => list.getEntries().map(({ name, startTime }) => { // 如何利用數據的邏輯 console.log(name, startTime) return startTime }) ) observer.observe({ entryTypes: ['mark'], buffered: true }) 複製代碼
聰慧的你應該發現了一些變化:
getEntries
而不是 getEntriesByType
;observe
方法時,設置了 entryTypes
和 buffer
由於在調用 observe
方法時設置了想要觀察的 entryTypes
,因此不須要再調用 getEntriesByType
。
buffered
字段的含義是,是否向 observer
的 buffer
中添加該條目(的 buffer
),默認值是 false
。
關於爲何會有
buffered
的設置,有興趣的讀者能夠參考 github.com/w3c/perform… [3]
PerformanceObserver
構造函數回過頭來看一看 PerformanceObserver
。
實例化時,接收一個參數,名爲 PerformanceObserverCallback
,顧名思義是一個回調函數。
該函數有兩個參數,分別是 PerformanceObserverEntryList
和 PerformanceObserver
。前者就是咱們關心的性能數據的集合。實際上咱們已經見過了好幾回,例如 performance.getEntriesByType('navigation')
就會返回這種數據類型;後者則是實例化對象,能夠理解爲函數提供了一個 this
值。
全部跟數據有關的具體操做,如上報、打印等,都可以在 PerformanceObserverCallback
中進行。
實例化後,返回一個 observer
對象。該對象具有兩個關鍵方法,分別是 observe
和 disconnect
。
observe
用於告訴瀏覽器,「我想觀察這幾類性能數據」;disconnect
用於斷開觀察,清空 buffer
。爲何會有
disconnect
方法?略具諷刺的一個事實是,長時間持續觀察性能數據,是一個比較消耗性能的行爲。所以,最好在「合適」的時間,中止觀察,清空對應buffer
,釋放性能。
使用 PerformanceObserver
+ performance.measure
對以前代碼進行重構:
// 在 measure.js 中 const getMeasure = () => { const observer = new PerformanceObserver(list => { list.getEntries().forEach(({ name, duration }) => { console.log(name, duration) // 操做數據的邏輯 }) }) // 只須要關注 measure 的數據 observer.observe({ entryTypes: ['measure'], buffered: true }) return observer } // 項目入口文件的頂部 let observer if (window.PerformanceObserver) { observer = getMeasure() } // 某一個合適的時間 再也不須要監控性能了 if (observer) { observer.disconnect() } 複製代碼
如此一來,獲取性能數據的操做實現了 DRY 。
假如屏幕前的你已經摩拳擦掌,躍躍欲試,且先緩一緩,看看如下幾點注意事項:
async
+ await
的形式監測「接口性能」(這裏強調的是用戶感知的性能,由於接口表現會顯著受到網絡環境、緩存使用、代理服務器等等的影響);User Agent
的)灰度;measure
一下。應當重點關注可能出現性能瓶頸的場景。且只有真正發生瓶頸時,再嘗試數據上報;Performance Timeline
+ User Timing
爲前端開發者提供了衡量代碼性能的利器。若是遇到的項目是「性能敏感」型,那麼儘早開始嘗試吧~
高峯、黃小璐、劉博文與李鬆峯等人對文章結構、細節提出了寶貴的修訂意見,排名分前後(時間順序),特此鳴謝。
《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公衆號後,直接發送連接到後臺便可給咱們投稿。