前端效能之Performance以及動畫幀率(FPS)
Performance
是一個做前端效能監控離不開的 API
,最好在頁面完全載入完成之後再使用,因為很多值必須在頁面完全載入之後才能得到。最簡單的辦法是在 window.onload
事件中讀取各種資料。
一、回顧頁面載入過程
要學習這套 API
的使用,先簡單介紹下前端的基礎知識
1.1 頁面載入
一個頁面的請求到響應再到顯示出來,需要經過下面一些重要過程,當我們在瀏覽器輸入一個 URL
或者說點選一個 URL
開始,會出現如下流程
- 頁面準備
- 重定向:在
header
定義了重定向才會有這個過程,如果沒有重定向,不會產生這個過程。 -
app cache
:會先檢查這個域名是否有快取,如果有快取就不需要DNS解析域名。這裡的app
是值應用程式application
,不指手機app
。 -
DNS
解析:把域名解析成IP
,如果直接用ip
地址訪問,不產生這個過程。 -
TCP
連線:http
協議是經過TCP
來傳輸的,所以產生一個http
請求就會有TCP connect
,但是依賴於長連線,不會產生這個過程。 -
request header
:請求頭資訊。 -
request body
:請求體資訊,比如get
請求是沒有請求體資訊的,所以沒有這個過程,這就是為什麼把頭跟體分開寫的原因。 -
response header
:響應頭資訊。 -
response body
:響應體資訊。 - 解析
HTML
結構 - 載入外部指令碼和樣式表文件:正常來說
JS
、css
都是外部載入的,當然有不正常的人啊,比如我。 - 解析並執行指令碼程式碼
- 構建與解析
HTML DOM
樹:這個過程可以去了解下DOM
樹是怎樣的就明白啦。 - 載入外部圖片
- 頁面載入完成,顯示出來啦
1.2 重定向分析
app cach DNS TCP request header app cach DNS TCP request header
二、performance
2.1 performance.timing
這個API能幫我們得到整個頁面請求的時間,如下圖,在 Chrome
的 Console
是可以直接執行的
先解釋下這些時間都是代表什麼
timing 物件裡邊的資料比較多,梳理如下幾個關鍵性的節點
-
fetchStart
:發起獲取當前文件的時間點,我的理解是瀏覽器收到發起頁面請求的時間點; -
domainLookupStart
:返回瀏覽器開始DNS
查詢的時間,如果此請求沒有DNS
查詢過程,如長連線、資源cache
、甚至是本地資源等,那麼就返回fetchStart
的值; -
domainLookupEnd
:返回瀏覽器結束DNS
查詢的時間,如果沒有DNS
查詢過程,同上; -
connectStart
:瀏覽器向伺服器請求文件,開始建立連線的時間,如果此連線是一個長連線,或者無需與伺服器連線(命中快取),則返回domainLookupEnd
的值; -
connectEnd
:瀏覽器向伺服器請求文件,建立連線成功的時間; -
requestStart
:開始請求文件的時間(注意沒有requestEnd
); -
responseStart
:瀏覽器開始接收第一個位元組資料的時間,資料可能來自於伺服器、快取、或本地資源; -
unloadEventStart
:解除安裝上一個文件開始的時間; -
unloadEventEnd
:解除安裝上一個文件結束的時間; -
domLoading
:瀏覽器把document.readyState
設定為“loading”
的時間點,開始構建dom
樹的時間點; -
responseEnd
:瀏覽器接收最後一個位元組資料的時間,或連線被關閉的時間; -
domInteractive
:瀏覽器把document.readyState設
置為“interactive”
的時間點,DOM
樹建立結束; -
domContentLoadedEventStart
:文件發生DOMContentLoaded
事件的時間; -
domContentLoadedEventEnd
:文件的DOMContentLoaded
事件結束的時間; -
domComplete
:瀏覽器把document.readyState
設定為“complete”
的時間點; -
loadEventStart
:文件觸發load
事件的時間; -
loadEventEnd
:文件出發load
事件結束後的時間
再來一張圖,表示各階段的開始與結束對應的時間
從以上的分析,我們就可以得到一些時間的計算
- 準備新頁面耗時:
fetchStart - navigationStart
- 重定向時間:
redirectEnd - redirectStart
-
App Cache
時間:domainLookupStart - fetchStart
-
DNS
解析時間:domainLookupEnd -domainLookupStart
-
TCP
連線時間:connectEnd - connectStart
-
request
時間:responseEnd - requestStart
這個計算是代表請求響應加起來的時間 - 請求完畢到
DOM
樹載入:domInteractive -responseEnd
- 構建與解析
DOM
樹,載入資源時間:domCompleter -domInteractive
-
load
時間:loadEventEnd - loadEventStart
- 整個頁面載入時間:
loadEventEnd -navigationStart
- 白屏時間:
responseStart-navigationStart
2.2 performance.getEntries()
這個API能幫我們獲得資源的請求時間,包括JS、CSS、圖片等
如上圖可以看到這個API請求返回的是一個數組,這個陣列包括整個頁面所有的資源載入,上圖打開了一個其中一個資源,可以看到如下資訊
-
entryType
:型別為resource
-
name
:資源的url
-
initiatorType
:資源是link
- 資源時間:
duration
的值,是responseEnd - startTime
得到的
2.3 performance.memory
這個API主要是得到瀏覽器記憶體情況
jsHeapSizeLimit totalJSHeapSize userdJSHeapSize
userdJSHeapSize
表示所有被使用的JS堆疊記憶體, totalJSHeapSize
可使用的JS堆疊記憶體,如果 userdJSHeapSize
的值大於 totalJSHeapSize
,就可能出現記憶體洩漏
三、動畫幀率FPS
3.1 流暢動畫的標準
FPS
表示的是每秒鐘畫面更新次數。我們平時所看到的連續畫面都是由一幅幅靜止畫面組成的,每幅畫面稱為一幀, FPS
是描述“幀”變化速度的物理量
- 理論上說,
FPS
越高,動畫會越流暢,目前大多數裝置的螢幕重新整理率為60
次/秒,所以通常來講FPS
為60 frame/s
時動畫效果最好,也就是每幀的消耗時間為16.6
7ms
不同幀率的體驗
50 ~ 60 FPS 30 ~ 50 FPS 30 FPS
3.2 獲取我們頁面動畫當前的 FPS 值
那麼我們該如何準確的獲取我們頁面動畫當前的 FPS 值呢?
3.2.1 方法一 藉助 Chrome 開發者工具
Chrome
提供給開發者的功能十分強大,在開發者工具中,我們進行如下選擇調出 FPS meter
選項:
通過這個按鈕,可以開啟頁面實時 Frame Rate
(幀率) 觀測及頁面 GPU
使用率
但是這個方法缺點太多了
- 這個只能一次觀測一到幾個頁面,而且需要人工實時觀測
- 資料只能是主觀感受,並沒有一個十分精確的資料不斷上報或者被收集
- 因此,我們需要更加智慧的方法。
3.2.2 方法二 藉助 Frame Timing API
Blink 核心早期架構
- 以
Chrome
瀏覽器核心Blink
渲染頁面為例。對早期的Chrome
瀏覽器而言,每個頁面Tab
對應一個獨立的renderer
程序,Renderer
程序中包含了主執行緒和合成執行緒。早期Chrome
核心架構
其中,主執行緒主要負責:
-
Javascript
的計算與執行 -
CSS
樣式計算 -
Layout
計算 - 將頁面元素繪製成點陣圖(
paint
),也就是光柵化(Raster
) - 將點陣圖給合成執行緒
合成執行緒則主要負責:
- 將點陣圖(
GraphicsLayer
層)以紋理(texture
)的形式上傳給GPU
- 計算頁面的可見部分和即將可見部分(滾動)
-
CSS
動畫處理 - 通知 GPU 繪製點陣圖到螢幕上
其實知道了這兩個執行緒之後,下一個概念是釐清 CSS
動畫與 JS
動畫的細微區別(當然它們都是 Web 動畫)
JS 動畫與 CSS 動畫的細微區別
- 對於
JS
動畫而言,它們執行時的幀率即是主執行緒和合成執行緒加起來消耗的時間。對於流暢動畫而言,我們希望它們每一幀的耗時保持在16.67ms
之內; - 而對於
CSS
動畫而言,由於其流程不受主執行緒的影響,所以希望能得到合成執行緒的消耗的時間,而合成執行緒的繪製頻率也反映了滾動和 CSS 動畫的流程性。
上面主要想得出的一個結論是。如果我們能夠知道主執行緒和合成執行緒每一幀消耗的時間,那麼我們就能大致得出對應的 Web 動畫的幀率。那麼上面說到的 Frame Timing API
是否可以幫助我們拿到這個時間點呢
什麼是 Frame Timing API ?
Frame Timing API
是 Web Performance Timing API
標準中的其中一位成員。 Web Performance Timing API
是 W3C 推出的一套效能 API 標準,用於幫助開發者對網站各方面的效能進行精確的分析與控制,提升 Web 網站效能
它包含許多子類 API,完成不同的功能,大致如下
怎麼使用呢?以 Navigation Timing
, Performance Timeline
, Resource Timing
為例子,對於相容它的瀏覽器,它以只讀屬性的形式對外暴露掛載在 window.performance
上。
我們再來回顧這張圖
通過這張圖以及上面的 window.performance.timing
,我們就可以輕鬆的統計出頁面每個重要節點的耗時,這就是 Web Performance Timing API
的強大之處,感興趣的可以詳細去研究研究,使用在頁面統計上
Frame Timing API 示意
終於可以迴歸正題,藉助 Web Performance Timing API
中的 Frame Timing API
,可以輕鬆的拿到每一幀中,主執行緒以及合成執行緒的時間。或者更加容易,直接拿到每一幀的耗時
獲取 Render
主執行緒和合成執行緒的記錄,每條記錄包含的資訊基本如下
var rendererEvents = window.performance.getEntriesByType("renderer"); var compositeThreadEvents = window.performance.getEntriesByType("composite");
或者是:
ar observer = new PerformanceObserver(function(list) { var perfEntries = list.getEntries(); for (var i = 0; i < perfEntries.length; i++) { console.log("frame: ", perfEntries[i]); } }); // subscribe to Frame Timing observer.observe({entryTypes: ['frame']});
每條記錄包含的資訊基本如下:
{ sourceFrameNumber: 120, startTime: 1342.549374253 cpuTime: 6.454313323 }
每個記錄都包括唯一的 Frame Number
、 Frame
開始時間以及 cpuTime
時間。通過計算每一條記錄的 startTime
,我們就可以算出每兩幀間的間隔,從而得到動畫的幀率是否能夠達到 60 FPS
看看 Web Performance Timing API 整體的相容性
Frame Timing API
雖好,但是,現在 Frame Timing API
的相容性不算很友好,額,不友好到什麼程度呢。還沒有任何瀏覽器支援,處於實驗性階段,屬於面向未來程式設計
3.2.3 方法三 藉助 requestAnimationFrame API
從上面的介紹,我們得知,如果我們可以到得到每一幀中的固定一個時間點,那麼兩者相減,也能夠近似得到一幀所消耗的時間
這次,我們藉助相容性不錯的 requestAnimationFrame API
// 語法 window.requestAnimationFrame(callback);
-
requestAnimationFrame
大家應該都不陌生,方法告訴瀏覽器您希望執行動畫並請求瀏覽器呼叫指定的函式在下一次重繪之前更新動畫。 - 當你準備好更新螢幕畫面時你就應用此方法。這會要求你的動畫函式在瀏覽器下次重繪前執行。回撥的次數常是每秒
60
次,大多數瀏覽器通常匹配W3C
所建議的重新整理率
使用 requestAnimationFrame 計算 FPS 原理
原理是,正常而言 requestAnimationFrame
這個方法在一秒內會執行 60
次,也就是不掉幀的情況下。假設動畫在時間 A
開始執行,在時間 B
結束,耗時 x ms
。而中間 requestAnimationFrame
一共執行了 n
次,則此段動畫的幀率大致為: n / (B - A)
核心程式碼如下,能近似計算每秒頁面幀率,以及我們額外記錄一個 allFrameCount
,用於記錄 rAF
的執行次數,用於計算每次動畫的幀率 :
var rAF = function () { return ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); } ); }(); var frame = 0; var allFrameCount = 0; var lastTime = Date.now(); var lastFameTime = Date.now(); var loop = function () { var now = Date.now(); var fs = (now - lastFameTime); var fps = Math.round(1000 / fs); lastFameTime = now; // 不置 0,在動畫的開頭及結尾記錄此值的差值算出 FPS allFrameCount++; frame++; if (now > 1000 + lastTime) { var fps = Math.round((frame * 1000) / (now - lastTime)); console.log(`${new Date()} 1S內 FPS:`, fps); frame = 0; lastTime = now; }; rAF(loop); } loop();
尋找一個有動畫不斷執行的頁面進行測試,可以看到程式碼執行如下:
- 使用了我之前製作的一個頁面進行了測試,使用
Chrome
同時調出頁面的FPS meter
,對比兩邊的實時FPS
值,基本吻合。 - 測試頁面,
Solar System
。你可以將上面的程式碼貼到這個頁面的console
中,測試一下資料
- 對比右上角的
Frame Rate
,幀率基本一致。在大部分情況下,這種方法可以很好的得出 Web 動畫的幀率。 - 如果我們需要統計某個特定動畫過程的幀率,只需要在動畫開始和結尾兩處分別記錄
allFrameCount
這個數值大小,再除以中間消耗的時間,也可以得出特定動畫過程的FPS
值。 - 值得注意的是,這個方法計算的結果和真實的幀率肯定是存在誤差的,因為它是將每兩次主執行緒執行
javascript
的時間間隔當成一幀,而非上面說的主執行緒加合成執行緒所消耗的時間為一幀。但是對於現階段而言,算是一種可取的方法