不同於傳統的 PC Web 或者是移動 WEB,在騰訊視訊客廳盒子端,接大屏顯示器(電視)下,許多能流暢執行於 PC 端、移動端的 Web 動畫,受限於硬體水平,在盒子端的表現的往往不盡如人意。
基於此,對於 Web 動畫的效能問題,僅僅停留在感覺已經優化的OK之上,是不夠的,想要在盒子端跑出高效能接近 60 FPS 的流暢動畫,就必須要刨根問底,深挖每一處可以提升的方法。
流暢動畫的標準
理論上說,FPS 越高,動畫會越流暢,目前大多數裝置的螢幕重新整理率為 60 次/秒,所以通常來講 FPS 為 60frame/s 時動畫效果最好,也就是每幀的消耗時間為 16.67ms。
直觀感受,不同幀率的體驗
- 幀率能夠達到 50 ~ 60 FPS 的動畫將會相當流暢,讓人倍感舒適;
- 幀率在 30 ~ 50 FPS 之間的動畫,因各人敏感程度不同,舒適度因人而異;
- 幀率在 30 FPS 以下的動畫,讓人感覺到明顯的卡頓和不適感;
- 幀率波動很大的動畫,亦會使人感覺到卡頓。
盒子端動畫優化
在騰訊視訊客廳盒子端,Web 動畫未進行優化之前,一些複雜動畫的幀率僅有 10 ~ 30 FPS,卡頓感非常明顯,帶來很不好的使用者體驗。
而進行優化之後,能將 10 ~ 30 FPS的動畫優化至 30 ~ 60 FPS,雖然不算優化到最完美,但是當前盒子硬體的條件下,已經算是非常大的進步。
盒子端 Web 動畫效能比較
首先先給出在盒子端不同型別的Web 動畫的效能比較。經過對比,在盒子端 CSS 動畫的效能要優於 Javascript 動畫,而在 CSS 動畫裡,使用 GPU 硬體加速的動畫效能要優於不使用硬體加速的效能。
所以在盒子端,實現一個 Web 動畫,優先順序是:
GPU 硬體加速 CSS 動畫 > 非硬體加速 CSS 動畫 > Javascript 動畫
動畫效能上報分析
要有優化,就必須得有資料做為支撐。對比優化前後是否有提升。而對於動畫而言,衡量一個動畫的標準也就是 FPS 值。
所以現在的關鍵是如何計算出每個動畫執行時的幀率,這裡我使用的是 requestAnimationFrame
這個函式近似的得到動畫執行時的幀率。
考慮到盒子都是安卓系統,且大多版本較低且硬體效能堪憂,導致一是許多高階 API 無法使用,二是這裡只是近似得到動畫幀率
原理是,正常而言 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 |
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('fps', fps); 每秒 FPS frame = 0; lastTime = now; }; rAF(loop); } |
研究結論
所以,我們的目標就是在使用 GPU 硬體加速的基礎之上,更深入的去優化 CSS 動畫,先給出最後的一個優化步驟方案:
- 精簡 DOM ,合理佈局
- 使用 transform 代替 left、top,減少使用耗效能樣式
- 控制頻繁動畫的層級關係
- 考慮使用 will-change
- 使用 dev-tool 時間線 timeline 觀察,找出導致高耗時、掉幀的關鍵操作
下文會有每一步驟的具體分析解釋。
Web 每一幀的渲染
要想達到 60 FPS,每幀的預算時間僅比 16 毫秒多一點 (1 秒/ 60 = 16.67 毫秒)。但實際上,瀏覽器有整理工作要做,因此您的所有工作需要儘量在 10 毫秒內完成。
而每一幀,如果有必要,我們能控制的部分,也是畫素至螢幕管道中的關鍵步驟如下:
完整的畫素管道 JS / CSS > 樣式 > 佈局 > 繪製 > 合成:
- JavaScript。一般來說,我們會使用 JavaScript 來實現一些視覺變化的效果。比如用 jQuery 的 animate 函式做一個動畫、對一個資料集進行排序或者往頁面裡新增一些 DOM 元素等。當然,除了 JavaScript,還有其他一些常用方法也可以實現視覺變化效果,比如:CSS Animations、Transitions 和 Web Animation API。
- 樣式計算。此過程是根據匹配選擇器(例如 .headline 或 .nav > .nav__item)計算出哪些元素應用哪些 CSS 3. 規則的過程。從中知道規則之後,將應用規則並計算每個元素的最終樣式。
- 佈局。在知道對一個元素應用哪些規則之後,瀏覽器即可開始計算它要佔據的空間大小及其在螢幕的位置。網頁的佈局模式意味著一個元素可能影響其他元素,例如 元素的寬度一般會影響其子元素的寬度以及樹中各處的節點,因此對於瀏覽器來說,佈局過程是經常發生的。
- 繪製。繪製是填充畫素的過程。它涉及繪出文字、顏色、影像、邊框和陰影,基本上包括元素的每個可視部分。繪製一般是在多個表面(通常稱為層)上完成的。
- 合成。由於頁面的各部分可能被繪製到多層,由此它們需要按正確順序繪製到螢幕上,以便正確渲染頁面。對於與另一元素重疊的元素來說,這點特別重要,因為一個錯誤可能使一個元素錯誤地出現在另一個元素的上層。
當然,不一定每幀都總是會經過管道每個部分的處理。我們的目標就是,每一幀的動畫,對於上述的管道流程,能避免則避免,不能避免則最大限度優化。
優化動畫步驟
先給出一個步驟,調優一個動畫,有一定的指導原則可以遵循,一步一步深入動畫:
1.精簡 DOM ,合理佈局
這個沒什麼好說的,如果可以,精簡 DOM 結構在任何時候都是對頁面有幫助的。
2.使用 transform 代替 left、top,減少使用耗效能樣式
現代瀏覽器在完成以下四種屬性的動畫時,消耗成本較低:
- position(位置):
transform: translate(npx, npx)
- scale(比例縮放):
transform: scale(n)
- rotation(旋轉) :
transform: rotate(ndeg)
- opacity(透明度):
opacity: 0...1
如果可以,儘量只使用上述四種屬性去控制動畫。
不同樣式在消耗效能方面是不同的,改變一些屬性的開銷比改變其他屬性要多,因此更可能使動畫卡頓。
例如,與改變元素的文字顏色相比,改變元素的 box-shadow
將需要開銷大很多的繪圖操作。 改變元素的 width
可能比改變其 transform
要多一些開銷。如 box-shadow
屬性,從渲染角度來講十分耗效能,原因就是與其他樣式相比,它們的繪製程式碼執行時間過長。
這就是說,如果一個耗效能嚴重的樣式經常需要重繪,那麼你就會遇到效能問題。其次你要知道,沒有不變的事情,在今天效能很差的樣式,可能明天就被優化,並且瀏覽器之間也存在差異。
開啟 GPU 硬體加速
歸根結底,上述四種屬性的動畫消耗較低的原因是會開啟了 GPU 硬體加速。動畫元素生成了自己的圖形層(GraphicsLayer)。
通常而言,開啟 GPU 加速的方法我們可以使用
will-change: transform
這會使宣告瞭該樣式屬性的元素生成一個圖形層,告訴瀏覽器接下來該元素將會進行 transform 變換,讓瀏覽器提前做好準備。
使用
will-change
並不一定會有效能的提升,因為即使瀏覽器預料到會有這些更改,依然會為這些屬性執行佈局和繪製流程,所以提前告訴瀏覽器,也並不會有太多效能上的提升。這樣做的好處是,建立新的圖層代價很高,而等到需要時匆忙地建立,不如一開始直接建立好。
對於 Safari 及一些舊版本瀏覽器,它們不能識別 will-change
,則需要使用某種 translate 3D 進行 hack,通常會使用
transform: translateZ(0)
所以,正常而言,在生產環境下,我們可能需要使用如下程式碼,開啟硬體加速:
1 2 3 4 |
{ will-change: transform; transform: translateZ(0); } |
3.控制頻繁動畫的層級關係
動畫層級的控制的意思是儘量讓需要進行 CSS 動畫的元素的 z-index
保持在頁面最上方,避免瀏覽器建立不必要的圖形層(GraphicsLayer),能夠很好的提升渲染效能。
OK,這裡又提到了圖形層(GraphicsLayer),這是一個瀏覽器渲染原理相關的知識(WebKit/blink核心下)。它能對動畫進行加速,但同時也存在相應的加速坑!
簡單來說,瀏覽器為了提升動畫的效能,為了在動畫的每一幀的過程中不必每次都重新繪製整個頁面。在特定方式下可以觸發生成一個合成層,合成層擁有單獨的 GraphicsLayer。
需要進行動畫的元素包含在這個合成層之下,這樣動畫的每一幀只需要去重新繪製這個 Graphics Layer 即可,從而達到提升動畫效能的目的。
那麼一個元素什麼時候會觸發建立一個 Graphics Layer 層?從目前來說,滿足以下任意情況便會建立層:
- 硬體加速的 iframe 元素(比如 iframe 嵌入的頁面中有合成層)
- 硬體加速的外掛,比如 flash 等等
- 使用加速視訊解碼的 <video>
元素
- 3D 或者 硬體加速的 2D Canvas 元素
- 3D 或透視變換 (perspective、transform) 的 CSS 屬性
- 對自己的 opacity 做 CSS 動畫或使用一個動畫變換的元素
- 擁有加速 CSS 過濾器的元素
- 元素有一個包含複合層的後代節點(換句話說,就是一個元素擁有一個子元素,該子元素在自己的層裡)
- 元素有一個 z-index 較低且包含一個複合層的兄弟元素
本小點中說到的動畫層級的控制,原因就在於上面生成層的最後一條:
元素有一個 z-index 較低且包含一個複合層的兄弟元素。
這裡是存在坑的地方,首先我們要明確兩點:
- 我們希望我們的動畫得到 GPU 硬體加速,所以我們會利用類似
transform: translateZ()
這樣的方式生成一個 Graphics Layer 層。 - Graphics Layer 雖好,但不是越多越好,每一幀的渲染核心都會去遍歷計算當前所有的 Graphics Layer ,並計算他們下一幀的重繪區域,所以過量的 Graphics Layer 計算也會給渲染造成效能影響。
記住這兩點之後,回到上面我們說的坑。
假設我們有一個輪播圖,有一個 ul 列表,結構如下:
1 2 3 4 5 6 7 8 9 |
<div class="container"> <div class="swiper">輪播圖</div> <ul class="list"> <li>列表li</li> <li>列表li</li> <li>列表li</li> <li>列表li</li> </ul> </div> |
假設給他們定義如下 CSS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
.swiper { position: static; animation: 10s move infinite; } .list { position: relative; } @keyframes move { 100% { transform: translate3d(10px, 0, 0); } } |
由於給 .swiper
新增了 translate3d(10px, 0, 0)
動畫,所以它會生成一個 Graphics Layer,如下圖所示,用開發者工具可以開啟層的展示,圖形外的黃色邊框即代表生成了一個獨立的複合層,擁有獨立的 Graphics Layer 。
但是!在上面的圖中,我們並沒有給下面的 list
也新增任何能觸發生成 Graphics Layer 的屬性,但是它也同樣也有黃色的邊框,生成了一個獨立的複合層。
原因在於上面那條元素有一個 z-index 較低且包含一個複合層的兄弟元素。我們並不希望 list
元素也生成 Graphics Layer ,但是由於 CSS 層級定義原因,下面的 list 的層級高於上面的 swiper,所以它被動的也生成了一個 Graphics Layer 。
使用 Chrome,我們也可以觀察到這種層級關係,可以看到 .list
的層級高於 .swiper
:
所以,下面我們修改一下 CSS ,改成:
1 2 3 4 5 6 7 8 |
.swiper { position: relative; z-index: 100; } .list { position: relative; } |
這裡,我們明確使得 .swiper
的層級高於 .list
,再開啟開發者工具觀察一下:
可以看到,這一次,.list
元素已經沒有了黃色外邊框,說明此時沒有生成 Graphics Layer 。再看看層級圖:
此時,層級關係才是我們希望看到的,.list
元素沒有觸發生成 Graphics Layer 。而我們希望需要硬體加速的 .swiper
保持在最上方,每次動畫過程中只會獨立重繪這部分的區域。
總結
這個坑最早見於張雲龍釋出的這篇文章CSS3硬體加速也有坑,這裡還要總結補充的是:
- GPU 硬體加速也會有坑,當我們希望使用利用類似
transform: translate3d()
這樣的方式開啟 GPU 硬體加速,一定要注意元素層級的關係,儘量保持讓需要進行 CSS 動畫的元素的z-index
保持在頁面最上方。 - Graphics Layer 不是越多越好,每一幀的渲染核心都會去遍歷計算當前所有的 Graphics Layer ,並計算他們下一幀的重繪區域,所以過量的 Graphics Layer 計算也會給渲染造成效能影響。
- 可以使用 Chrome ,用上面介紹的兩個工具對自己的頁面生成的 Graphics Layer 和元素層級進行觀察然後進行相應修改。
- 上面觀察頁面層級的 chrome 工具非常吃記憶體?好像還是一個處於實驗室的功能,分析稍微大一點的頁面容易直接卡死,所以要多學會使用第一種觀察黃色邊框的方式檢視頁面生成的 Graphics Layer 這種方式。
4. 使用 will-change 可以在元素屬性真正發生變化之前提前做好對應準備
1 2 3 4 |
// 示例 .example { will-change: transform; } |
上面已經提到過 will-change 了。
will-change 為 web 開發者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發生變化之前提前做好對應的優化準備工作。 這種優化可以將一部分複雜的計算工作提前準備好,使頁面的反應更為快速靈敏。
值得注意的是,用好這個屬性並不是很容易:
- 在一些低端盒子上,
will-change
會導致很多小問題,譬如會使圖片模糊,有的時候很容易適得其反,所以使用的時候還需要多加測試。 - 不要將 will-change 應用到太多元素上:瀏覽器已經盡力嘗試去優化一切可以優化的東西了。有一些更強力的優化,如果與 will-change 結合在一起的話,有可能會消耗很多機器資源,如果過度使用的話,可能導致頁面響應緩慢或者消耗非常多的資源。
- 有節制地使用:通常,當元素恢復到初始狀態時,瀏覽器會丟棄掉之前做的優化工作。但是如果直接在樣式表中顯式宣告瞭 will-change 屬性,則表示目標元素可能會經常變化,瀏覽器會將優化工作儲存得比之前更久。所以最佳實踐是當元素變化之前和之後通過指令碼來切換 will-change 的值。
- 不要過早應用 will-change 優化:如果你的頁面在效能方面沒什麼問題,則不要新增 will-change 屬性來榨取一丁點的速度。 will-change 的設計初衷是作為最後的優化手段,用來嘗試解決現有的效能問題。它不應該被用來預防效能問題。過度使用 will-change 會導致生成大量圖層,進而導致大量的記憶體佔用,並會導致更復雜的渲染過程,因為瀏覽器會試圖準備可能存在的變化過程,這會導致更嚴重的效能問題。
- 給它足夠的工作時間:這個屬性是用來讓頁面開發者告知瀏覽器哪些屬性可能會變化的。然後瀏覽器可以選擇在變化發生前提前去做一些優化工作。所以給瀏覽器一點時間去真正做這些優化工作是非常重要的。使用時需要嘗試去找到一些方法提前一定時間獲知元素可能發生的變化,然後為它加上 will-change 屬性。
5. 使用 dev-tool 時間線 timeline 觀察,找出導致高耗時、掉幀的關鍵操作
1)對比螢幕快照,觀察每一幀包含的內容及具體的操作
2)找到掉幀的那一幀,分析該幀內不同步驟的耗時佔比,進行有針對性的優化
3)觀察是否存在記憶體洩漏
對於 timeline 的使用用法,這裡有個非常好的教程,通俗易懂,可以看看:
總結一下
對於盒子端 CSS 動畫的效能,很多方面仍處於探索中,本文大量內容在之前文章已經出現過,這裡更多的是歸納總結提煉成可參照執行的流程。
本文的優化方案研究同樣適用於 PC Web 及移動 Web,文章難免有錯誤及疏漏,歡迎不吝賜教。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式