我們知道,動畫其實是由一幀一幀的影像構成的。有 Web 動畫那麼就會存在該動畫在播放執行時的幀率。而幀率在不同裝置不同情況下又是不一樣的。
有的時候,一些複雜或者重要動畫,我們需要實時監控它們的幀率,或者說是需要知道它們在不同裝置的執行狀況,從而更好的優化它們,本文就是介紹 Web 動畫幀率(FPS)計算方法。
流暢動畫的標準
首先,理清一些概念。FPS 表示的是每秒鐘畫面更新次數。我們平時所看到的連續畫面都是由一幅幅靜止畫面組成的,每幅畫面稱為一幀,FPS 是描述“幀”變化速度的物理量。
理論上說,FPS 越高,動畫會越流暢,目前大多數裝置的螢幕重新整理率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時動畫效果最好,也就是每幀的消耗時間為 16.67ms。
當然,經常玩 FPS 遊戲的朋友肯定知道,吃雞/CSGO 等 FPS 遊戲推薦使用 144HZ 重新整理率的顯示器,144Hz 顯示器特指每秒的重新整理率達到 144Hz 的顯示器。相較於普通顯示器每秒60的重新整理速度,畫面顯示更加流暢。因此144Hz顯示器比較適用於視角時常保持高速運動的第一人稱射擊遊戲。
不過,這個只是顯示器提供的高重新整理率特性,對於我們 Web 動畫而言,是否支援還要看瀏覽器,而大多數瀏覽器重新整理率為 60 次/秒。
直觀感受,不同幀率的體驗:
- 幀率能夠達到 50 ~ 60 FPS 的動畫將會相當流暢,讓人倍感舒適;
- 幀率在 30 ~ 50 FPS 之間的動畫,因各人敏感程度不同,舒適度因人而異;
- 幀率在 30 FPS 以下的動畫,讓人感覺到明顯的卡頓和不適感;
- 幀率波動很大的動畫,亦會使人感覺到卡頓。
OK,那麼我們該如何準確的獲取我們頁面動畫當前的 FPS 值呢?
法一:藉助 Chrome 開發者工具
Chrome 提供給開發者的功能十分強大,在開發者工具中,我們進行如下選擇調出 FPS meter 選項:
通過這個按鈕,可以開啟頁面實時 Frame Rate (幀率) 觀測及頁面 GPU 使用率。
缺點
但是這個方法缺點太多了,
- 這個只能一次觀測一到幾個頁面,而且需要人工實時觀測
- 資料只能是主觀感受,並沒有一個十分精確的資料不斷上報或者被收集
因此,我們需要更加智慧的方法。
法二:藉助 Frame Timing API
在介紹下面這種方法前,繼續做一些基礎知識的科普。
Blink 核心早期架構
以 Chrome 瀏覽器核心 Blink 渲染頁面為例。對早期的 Chrome 瀏覽器而言,每個頁面 Tab 對應一個獨立的 renderer 程式,Renderer 程式中包含了主執行緒和合成執行緒。早期 Chrome 核心架構:
其中,主執行緒主要負責:
- Javascript 的計算與執行
- CSS 樣式計算
- Layout 計算
- 將頁面元素繪製成點陣圖(paint),也就是光柵化(Raster)
- 將點陣圖給合成執行緒
合成執行緒則主要負責:
- 將點陣圖(GraphicsLayer 層)以紋理(texture)的形式上傳給 GPU
- 計算頁面的可見部分和即將可見部分(滾動)
- CSS 動畫處理
- 通知 GPU 繪製點陣圖到螢幕上
OK,雲裡霧裡的,什麼東西。其實知道了這兩個執行緒之後,下一個概念是釐清 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,完成不同的功能,大致如下(摘自使用效能API快速分析web前端效能,當然你也可以看英文原版介紹:Web Performance Timing API ):
怎麼使用呢?以 Navigation Timing, Performance Timeline, Resource Timing
為例子,對於相容它的瀏覽器,它以只讀屬性的形式對外暴露掛載在 window.performance
上。
在除錯臺 console 中列印 window.performance
,檢視其中的 timing 屬性:
這物件內一連串的變數表示什麼呢,它表示我們頁面整個載入過程中每一個重要的時間點,可以詳細看看這張圖:
通過這張圖以及上面的 window.performance.timing
,我們就可以輕鬆的統計出頁面每個重要節點的耗時,這就是 Web Performance Timing API 的強大之處,感興趣的可以詳細去研究研究,使用在頁面統計上。
Frame Timing API 示意
好的,終於可以迴歸正題,藉助 Web Performance Timing API 中的 Frame Timing API,可以輕鬆的拿到每一幀中,主執行緒以及合成執行緒的時間。或者更加容易,直接拿到每一幀的耗時。
獲取 Render 主執行緒和合成執行緒的記錄,每條記錄包含的資訊基本如下,程式碼示意,(參考至Developer feedback needed: Frame Timing API):
1 2 |
var rendererEvents = window.performance.getEntriesByType("renderer"); var compositeThreadEvents = window.performance.getEntriesByType("composite"); |
或者是:
1 2 3 4 5 6 7 8 9 |
var 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']}); |
每條記錄包含的資訊基本如下:
1 2 3 4 5 |
{ 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 的相容性不算很友好,額,不友好到什麼程度呢。還沒有任何瀏覽器支援,處於實驗性階段,屬於面向未來程式設計。這你 TM 逗我呢?說了這麼久完全不能用…..
法三:藉助 requestAnimationFrame API
費了這麼多筆墨描述 Frame Timing API 但最後因為相容性問題完全沒辦法使用。不過不代表這麼長篇幅的描述沒有用,從上面的介紹,我們得知,如果我們可以到得到每一幀中的固定一個時間點,那麼兩者相減,也能夠近似得到一幀所消耗的時間。
那麼,我們再另闢蹊徑。這次,我們藉助相容性不錯的 requestAnimationFrame API。
1 2 |
// 語法 window.requestAnimationFrame(callback); |
requestAnimationFrame
大家應該都不陌生,方法告訴瀏覽器您希望執行動畫並請求瀏覽器呼叫指定的函式在下一次重繪之前更新動畫。
當你準備好更新螢幕畫面時你就應用此方法。這會要求你的動畫函式在瀏覽器下次重繪前執行。回撥的次數常是每秒 60 次,大多數瀏覽器通常匹配 W3C 所建議的重新整理率。
使用 requestAnimationFrame 計算 FPS 原理
原理是,正常而言 requestAnimationFrame 這個方法在一秒內會執行 60 次,也就是不掉幀的情況下。假設動畫在時間 A 開始執行,在時間 B 結束,耗時 x ms。而中間 requestAnimationFrame 一共執行了 n 次,則此段動畫的幀率大致為:n / (B – A)。
核心程式碼如下,能近似計算每秒頁面幀率,以及我們額外記錄一個 allFrameCount,用於記錄 rAF 的執行次數,用於計算每次動畫的幀率 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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(); |
OK,尋找一個有動畫不斷執行的頁面進行測試,可以看到程式碼執行如下:
這裡,我使用了我之前製作的一個頁面進行了測試,使用 Chrome 同時調出頁面的 FPS meter,對比兩邊的實時 FPS 值,基本吻合。
測試頁面,Solar System。你可以將上面的程式碼貼到這個頁面的 console 中,測試一下資料:
對比右上角的 Frame Rate,幀率基本一致。在大部分情況下,這種方法可以很好的得出 Web 動畫的幀率。
如果我們需要統計某個特定動畫過程的幀率,只需要在動畫開始和結尾兩處分別記錄 allFrameCount
這個數值大小,再除以中間消耗的時間,也可以得出特定動畫過程的 FPS 值。
值得注意的是,這個方法計算的結果和真實的幀率肯定是存在誤差的,因為它是將每兩次主執行緒執行 javascript 的時間間隔當成一幀,而非上面說的主執行緒加合成執行緒所消耗的時間為一幀。但是對於現階段而言,算是一種可取的方法。
參考文章
- A Primer for Web Performance Timing APIs
- Frame Timing
- Developer feedback needed: Frame Timing API
- 無線效能優化:FPS 測試
- 使用效能API快速分析web前端效能
好了,本文到此結束,希望對你有幫助 :)
如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式