編者按:本文作者是來自360奇舞團的前端開發工程師劉宇晨,同時也是 W3C 效能工作組成員。
上一回,筆者介紹了 Navigation Timing
和 Resource Timing
在監控頁面載入上的實際應用。
這一回,筆者將帶領大家學習 Performance Timeline
和 User Timing
標準,並使用相應的 API,給前端程式碼“跑個分”。
為什麼要學習這兩個標準?
真實業務中,時而會出現比較消耗效能的操作,特別是頻繁操作 DOM 的行為。那麼如何量化這些操作的效能表現呢?
常見的做法,就是通過分別記錄函式執行前和執行之後的 Date.now()
,然後求差,得出具體的執行時間。
記錄一兩個函式還好,多了的話,還需要開發者維護一個全域性的 hash ,用來統計全部資料。
隨著 Performance Timeline
+ User Timing
標準的推出,開發者可以直接使用相應的 API,瀏覽器便會直接統計相關資訊,從而顯著簡化了衡量前端效能的流程。
什麼是 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
一下。應當重點關注可能出現效能瓶頸的場景。且只有真正發生瓶頸時,再嘗試資料上報; - 本文目的,並非想要替代各個 benchmark 庫。實際上,基礎的效能測試還是應當由 benchmark 庫完成。本文的關注點,更多在於“監控”,即使用者體驗,發現真實發生的效能瓶頸場景。
總結
Performance Timeline
+ User Timing
為前端開發者提供了衡量程式碼效能的利器。如果遇到的專案是“效能敏感”型,那麼儘早開始嘗試吧~
鳴謝
高峰、黃小璐、劉博文與李鬆峰等人對文章結構、細節提出了寶貴的修訂意見,排名分先後(時間順序),特此鳴謝。
文內連結
關於奇舞週刊
《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社群。關注公眾號後,直接傳送連結到後臺即可給我們投稿。