代碼快不快?跑個分就知道

編者按:本文做者是來自360奇舞團的前端開發工程師劉宇晨,同時也是 W3C 性能工做組成員。前端

上一回,筆者介紹了 Navigation TimingResource Timing 在監控頁面加載上的實際應用。git

這一回,筆者將帶領你們學習 Performance TimelineUser 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 TimelineUser Timing 的最新標準均爲 Level 2,且均處於編輯草稿狀態。異步

瀏覽器兼容性

圖爲 Performance Timeline Level 2PerformanceObserver API 的支持狀況:

Performance Timeline Level 2 在實際應用時,主要使用 PerformanceObserver API。

Performance Timeline

圖爲 User Timing 的支持狀況:

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 方法接收三個參數,依次是 measureNamestartMark 以及 endMark

startMarkendMark 很容易理解,就是對應開始和結束時的 markNamemeasureName 則是爲每個 measure 行爲,提供一個標識。

調用後,performance.measure 會根據 startMarkendMark 對應的兩條記錄(均由 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 方法時,設置了 entryTypesbuffer

由於在調用 observe 方法時設置了想要觀察的 entryTypes,因此不須要再調用 getEntriesByType

buffered 字段的含義是,是否向 observerbuffer 中添加該條目(的 buffer),默認值是 false

關於爲何會有 buffered 的設置,有興趣的讀者能夠參考 github.com/w3c/perform… [3]

PerformanceObserver 構造函數

回過頭來看一看 PerformanceObserver

實例化時,接收一個參數,名爲 PerformanceObserverCallback,顧名思義是一個回調函數。

該函數有兩個參數,分別是 PerformanceObserverEntryListPerformanceObserver。前者就是咱們關心的性能數據的集合。實際上咱們已經見過了好幾回,例如 performance.getEntriesByType('navigation') 就會返回這種數據類型;後者則是實例化對象,能夠理解爲函數提供了一個 this 值。

全部跟數據有關的具體操做,如上報、打印等,都可以在 PerformanceObserverCallback 中進行。

實例化後,返回一個 observer 對象。該對象具有兩個關鍵方法,分別是 observedisconnect

  • 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 爲前端開發者提供了衡量代碼性能的利器。若是遇到的項目是「性能敏感」型,那麼儘早開始嘗試吧~

鳴謝

高峯、黃小璐、劉博文與李鬆峯等人對文章結構、細節提出了寶貴的修訂意見,排名分前後(時間順序),特此鳴謝。

文內連接

  1. w3c.github.io/performance…

  2. www.w3.org/TR/user-tim…

  3. github.com/w3c/perform…

關於奇舞週刊

《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公衆號後,直接發送連接到後臺便可給咱們投稿。

相關文章
相關標籤/搜索