你的函數(shù)有多快?使用 Performance 監(jiān)控前端性能
要比較兩個函數(shù)哪個性能更好,一個直觀且公平的方法就是計算兩個函數(shù)分別執(zhí)行完的時間。
良好的性能更容易好的用戶體驗,而好的用戶體驗更能留住用戶。研究表明,由于性能問題,在88%的在線消費者對用戶體驗不滿意后,他們不太可能會二次使用。
這也是為什么要提高性能的一個重要原因。特別是使用 JS 開發(fā)時,編寫的每一行 JS 都可能會阻塞DOM,因為它是單線程語言。
本次分享,我們主要介紹如何計算函數(shù)的性能。
Performance.now
Performance是一個做前端性能監(jiān)控離不開的API,最好在頁面完全加載完成之后再使用,因為很多值必須在頁面完全加載之后才能得到。最簡單的辦法是在window.onload事件中讀取各種數(shù)據(jù)。
performance.now()方法返回一個精確到毫秒的 DOMHighResTimeStamp 。
根據(jù) MDN :
這個時間戳實際上并不是高精度的。為了降低像Spectre這樣的安全威脅,各類瀏覽器對該類型的值做了不同程度上的四舍五入處理。(Firefox從Firefox 59開始四舍五入到2毫秒精度)一些瀏覽器還可能對這個值作稍微的隨機化處理。這個值的精度在未來的版本中可能會再次改善;瀏覽器開發(fā)者還在調(diào)查這些時間測定攻擊和如何更好的緩解這些攻擊。
因為,要計算一個函數(shù)的執(zhí)行時間,分別比較函數(shù)執(zhí)行前和執(zhí)行后的兩次 performance.now()的值即可,如下所示:
- const t0 = performance.now();
- for (let i = 0; i < array.length; i++)
- {
- // some code
- }
- const t1 = performance.now();
- console.log(t1 - t0, 'milliseconds');
在這里,我們可以看到 Firefox 中的結(jié)果與 Chrome 完全不同。這是因為從版本60開始,F(xiàn)irefox 將performance API的精度降低到2ms。
performance API 不當當只有返回時間戳這個功能,還有很多實用方法,大家可以根據(jù)需要到 MDN 查詢相關(guān)的文檔。
然而,對于我們的用例,我們只想計算單個函數(shù)的性能,因此時間戳就足夠了。
performance.now() 和 Date.now一樣嗎?
你可能會想,嘿,我也可以使用Date.now來做?
是的,你可以,但這有缺點。
Date.now返回自Unix紀元(1970-01-01T00:00:00Z)以來經(jīng)過的時間(以毫秒為單位),并取決于系統(tǒng)時鐘。這不僅意味著它不夠精確,而且還不總是遞增。WebKit工程師(Tony Gentilcore)的解釋如下:
基于系統(tǒng)時間的日期可能不太會被采用,對于實際的用戶監(jiān)視也不是理想的選擇。大多數(shù)系統(tǒng)運行一個守護程序,該守護程序定期同步時間。通常每15至20分鐘將時鐘調(diào)整幾毫秒。以該速率,大約10秒間隔的1%將是不準確的。
Performance.mark 和 Performance.measure
除了Performance.now函數(shù)外,還有一些函數(shù)可以讓我們度量代碼不同部分的時間,并將它們作為性能測試工具(如Webpagetest)中的自定義度量。
Performance.mark
先來看看MDN中關(guān)于mark方法的定義:
- The mark() method creates a timestamp in the browser's performance entry buffer with the given name.
這段話可以分解出三個關(guān)鍵詞。首先timestamp,這里的timestamp指的是高精度時間戳(千分之一毫秒),其次是performance entry buffer。
performance entry buffer指的是存儲performance實例對象的區(qū)域,初始值為空。
最后就是given name,表示生成的每一個timestamp都有相應(yīng)的名稱。
所以這句話就可以理解成,在瀏覽器的performance entry buffer中,根據(jù)名稱生成高精度時間戳。也就是很多人說過的**“打點”**。
就像Performance.now一樣,此函數(shù)的精度分數(shù)高達5µs。
- performance.mark('name');
標記 的 performance entry將具有以下屬性值:
- entryType - 設(shè)置為 "mark".
- name - 設(shè)置為mark被創(chuàng)建時給出的 "name"
- startTime - 設(shè)置為 mark() 方法被調(diào)用時的 timestamp 。
- duration - 設(shè)置為 "0" (標記沒有持續(xù)時間).
Performance.measure
同樣先來看看 MDN 上關(guān)于 measure 的定義:
這段定義和上面 mark 的定義有些類似,其最核心的不同點在于這句話 between two specified marks。所以measure是指定兩個mark點之間的時間戳。如果說mark可以理解為**"打點"的話,measure就可以理解為"連線"**。
- performance.measure(name, startMark, endMark);
計算兩個mark之間的時長,創(chuàng)建一個DOMHighResTimeStamp保存在資源緩存數(shù)據(jù)中,可通過performance.getEntries()等相關(guān)接口獲取。
- entryType 為字符串 measure
- name 為創(chuàng)建時設(shè)置的值
- startTime為調(diào)用 measure 時的時間
- duration為兩個 mark 之間的時長
從導航開始測量
- performance.measure('measure name');
導航開始到標記
- performance.measure('measure name', undefined, 'mark-2');
從標記到標記
- performance.measure('measure name', 'mark-1', 'mark-2');
資源性能數(shù)據(jù)
從 performance entry buffer 獲取數(shù)據(jù)
在上面的函數(shù)中,總是提到結(jié)果存儲在performance entry buffer,但是如何訪問其中的內(nèi)容呢?
performance API有3個函數(shù)可以用來訪問該數(shù)據(jù):
performance.getEntries()
獲取一組當前頁面已經(jīng)加載的資源PerformanceEntry對象。接收一個可選的參數(shù)options進行過濾,options支持的屬性有name,entryType,initiatorType。
- let entries = window.performance.getEntries();
performance.getEntriesByName
根據(jù)參數(shù)name,type獲取一組當前頁面已經(jīng)加載的資源數(shù)據(jù)。name的取值對應(yīng)到資源數(shù)據(jù)中的name字段,type取值對應(yīng)到資源數(shù)據(jù)中的entryType字段。
- let entries = window.performance.getEntriesByName(name, type);
performance.getEntriesByType
根據(jù)參數(shù)type獲取一組當前頁面已經(jīng)加載的資源數(shù)據(jù)。type取值對應(yīng)到資源數(shù)據(jù)中的entryType字段。
- var entries = window.performance.getEntriesByType(type);
結(jié)合事例:
- performance.mark('mark-1');
- // some code
- performance.mark('mark-2')
- performance.measure('test', 'mark-1', 'mark-2')
- console.log(performance.getEntriesByName('test')[0].duration);
Console.time
這個 API確實易于使用。當需要統(tǒng)計一段代碼的執(zhí)行時間時,可以使用console.time方法與console.timeEnd方法,其中console.time方法用于標記開始時間,console.timeEnd方法用于標記結(jié)束時間,并且將結(jié)束時間與開始時間之間經(jīng)過的毫秒數(shù)在控制臺中輸出。這兩個方法的使用方法如下所示。
- console.time('test');
- for (let i = 0; i < array.length; i++) {
- // some code
- }
- console.timeEnd('test');
輸出的結(jié)果與Performance API非常相似。
console.time的優(yōu)點是易于使用,因為它不需要手動計算兩個時間戳之間的差。
減少時間精度
如果在不同的瀏覽器中使用上面提到的 api 測量函數(shù),你可能會注意到結(jié)果是不同的。
這是由于瀏覽器試圖保護用戶免受時序攻擊(timing attack)和指紋采集(Fingerprinting ),如果時間戳過于準確,黑客可以使用它們來識別用戶。
例如,F(xiàn)irefox等瀏覽器試圖通過將精度降低到2ms(版本60)來防止這種情況發(fā)生。
注意事項
現(xiàn)在,我們已經(jīng)知道了要測量JavaScript函數(shù)的速度所需方法。但是,最好還要避免一些陷阱:
分而治之
開發(fā)過程中,我們可能會我發(fā)現(xiàn)有些模塊執(zhí)行速度很慢,但是我們不知道具體問題出在哪里。解決一個方法是,使用上面提到的這些函數(shù)來測量它,而不是胡亂猜測代碼的哪一部分比較慢。
要對其進行跟蹤,首先將console.time語句放在執(zhí)行比較慢的代碼塊周圍。然后測量它們不同部分的表現(xiàn)。如果一個比另一個慢,那就繼續(xù)往下走,直到發(fā)現(xiàn)問題所在。
注意輸入值
在實際應(yīng)用中,給定函數(shù)的輸入值可能會發(fā)生很大變化。僅針對任意隨機值測量函數(shù)的速度并不能提供我們可以實際使用的任何有價值的數(shù)據(jù)。
確保使用相同的輸入值運行代碼。
多次運行該函數(shù)
假設(shè)你有一個函數(shù),它的功是遍歷一個數(shù)組,對數(shù)組的每個值進行一些計算,然后返回一個帶有結(jié)果的數(shù)組。你想知道是forEach循環(huán)還是簡單的for循環(huán)性能更好。
- function testForEach(x) {
- console.time('test-forEach');
- const res = [];
- x.forEach((value, index) => {
- res.push(value / 1.2 * 0.1);
- });
- console.timeEnd('test-forEach')
- return res;
- }
- function testFor(x) {
- console.time('test-for');
- const res = [];
- for (let i = 0; i < x.length; i ++) {
- res.push(x[i] / 1.2 * 0.1);
- }
- console.timeEnd('test-for')
- return res;
- }
然后這樣測試它們:
- const x = new Array(100000).fill(Math.random());
- testForEach(x);
- testFor(x);
如果在 Firefox 中運行上述函數(shù),結(jié)果:
看起來forEach慢多了,對吧?
那如果是相同的輸入,運行兩次呢:
- testForEach(x);
- testForEach(x);
- testFor(x);
- testFor(x);
如果我們第二次調(diào)用forEach測試,它的執(zhí)行效果和for循環(huán)一樣好??紤]到初始值較慢,在一些性能要求極高的項目,可能就不適合使用forEach。
在多個瀏覽器中測試
如果我們在Chrome中運行上述代碼,結(jié)果又會不一樣:
這是因為Chrome和Firefox具有不同的JavaScript引擎,它們具有不同類型的性能優(yōu)化。
在本例中,F(xiàn)irefox 在對相同輸入的forEach進行優(yōu)化方面做得更好。
for在兩個引擎上的性能都更好,因此在一些性能要求極高的項目就需要使用for循環(huán)。
這是為什么要在多個引擎中進行測量的一個很好的例子。如果僅使用Chrome進行測量,你可能會得出結(jié)論,與for相比,forEach并不那么糟糕。
限制的 CPU
我們在本地測試值是不能代表用戶在瀏覽器使用的情況,因為 我們開發(fā)的電腦一般都會比大部分的用戶好很多。
瀏覽器有一個特性可以限制CPU性能,我們通過設(shè)置可以更貼切一些真實情況。
總結(jié)
在本文中,我們看到了一些JavaScript API,我們可以使用它們來衡量性能,以及如何在真實的項目中使用它們。對于簡單的測量,我發(fā)現(xiàn)使用console.time更容易。如果要將測量與性能測量工具集成在一起,則可能需要使用performance.mark和performance.measure。
譯者:前端小智 作者:Felix Gerschau 來源:felixgerschau 原文:https://felixgerschau.com/measuring-the-performance-of-java-script-functions
本文轉(zhuǎn)載自微信公眾號「大遷世界」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系大遷世界公眾號。