寫在前面
滿世界的動畫性能優化技巧,例如:
-
只允許改變
transform、opacity,其它屬性不要動,避免重新計算佈局(reflow) -
對動畫元素應用
transform: translate3d(0, 0, 0)、will-change: transform等,開啟硬體加速 -
動畫元素盡量用
fixed、absolute定位方式,避免 reflow -
對動畫元素應用高一點的
z-index,減少複合層數量 -
。。。其它可能有用的規則
那麼問題是:已經小心遵守這些規則了,為什麼動畫還會卡頓、跳幀?還能優化嗎?要從哪裡入手?
一.硬體加速是非規範的
The most important thing I'd like to tell you before we dive deep into GPU compositing is this: It's a giant hack. You won't find anything (at least for now) in the W3C's specifications about how compositing works, about how to explicitly put an element on a compositing layer or even about compositing itself. It's just an optimization that the browser applies to perform certain tasks and that each browser vendor implements in its own way.
很多情況下,開啟硬體加速確實能帶來明顯的性能提升,但是,這部分內容是非規範的,W3C 沒有相關規範說明其中細節,所以通過一些技巧(例如 transform: translate3d(0, 0, 0))開啟硬體加速是規範之外的行為,可能得到性能提升,也可能帶來嚴重的性能問題
也許在將來會形成規範,依照規範去做肯定能獲得性能提升,但在這之前,除了必須遵從各項性能優化原則外,還要考慮實際渲染流程,從原理上解決性能問題
硬體加速(Hardware Acceleration)
硬體加速在 CSS 動畫上是指 GPU 複合(GPU compositing),瀏覽器不直接通過 CPU 生成圖像數據顯示出來,而是把相關層數據發送給 GPU,而 GPU 在圖像數據運算方面有天生優勢,所以算是加速
那麼當硬體加速不可用時,瀏覽器怎樣渲染頁面?
在沒有硬體加速的情況下,瀏覽器通常是依賴於 CPU 來渲染生成網頁的內容,大致的做法是遍歷這些層,然後按照順序把這些層的內容依次繪製在一個內部存儲空間上(例如 bitmap),最後把這個內部表示顯示出來,這種做法就是軟體渲染(software rendering)
二.transform 和 opacity 的特殊性
以前通過改變佈局相關屬性形成動畫,例如:
@keyframes move {
from { left: 30px; }
to { left: 100px; }
}
對於動畫的每一幀,瀏覽器都要重新計算元素的形狀位置(reflow),把新狀態渲染出來(repaint),再顯示到螢幕上
整頁 reflow 和 repaint 想想就覺得���慢,那麼如果把動畫元素抽出來作為前景,每幀其它部分作為背景不變,只重新渲染動畫元素,再把前景背景複合起來,是不是會更快?當然會,因為 GPU 能快速地進行亞像素級圖層複合
但是這樣做的前提是能夠按照動的、不動的劃分出前景背景層,如果動畫元素或者受佈局影響,或者動的過程中影響到了佈局,就會打破前景背景的界限,這樣簡單分為 2 層就有問題。那麼,應用 position: fixed | absolute 是不是就能保證不會影響佈局了?
不行,因為 left 可以接受百分比值、相對單位(em、vw 等等),瀏覽器不能百分百肯定該屬性的變化與佈局無關,所以不能簡單的分出前景背景層,例如:
@keyframes move {
from { left: 30px; }
to { left: 100%; }
}
但瀏覽器能百分百肯定 transform 和 opacity 的變化與佈局無關,不受佈局影響,其變化也不會影響現有佈局,所以這兩個屬性的特殊性是:
-
does not affect the document's flow,
-
does not depend on the document's flow,
-
does not cause a repaint.
如果不影響佈局,且不受佈局影響,其變化不會導致其它部分需要 repaint,那麼這個東西肯定可以抽出去單獨作為一層,放心交給 GPU 去處理,享受硬體加速帶來的好處:
-
細膩(GPU 能做到亞像素級精度,且對 GPU 來說不費勁)
-
流暢(不受其它運算密集的 JS 任務影響,動畫交給 GPU 了,與 CPU 無關)
三.GPU 複合的代價
It might surprise you, but the GPU is a separate computer. That's right: An essential part of every modern device is actually a standalone unit with its own processors and its own memory- and data-processing models. And the browser, like any other app or game, has to talk with the GPU as it would with an external device.
GPU 是獨立的一部分,有自己的處理器、記憶體核數據處理模型,那麼意味著通過 CPU 在記憶體裡創建的圖像數據無法直接與 GPU 共享,需要打包發送給 GPU,GPU 收到後才能執行我們期望的一系列操作,這個過程需要時間,而打包數據需要記憶體
需要的記憶體取決於:
-
複合層的數量
-
複合層的大小
相對於數量,複合層的大小影響更大一些,例如:
.rect {
width: 320px;
height: 240px;
background: #f00;
}
這個紅塊如果要發送給 GPU 的話,需要的存儲空間是:320 × 240 × 3 = 230400B = 225KB(rgb 需要 3 個位元組),如果圖像含有透明部分,就需要 320 × 240 × 4 = 307200B = 300KB
這樣一個不起眼的小紅塊就需要 2、300KB,頁面動輒幾十上百個元素,佔全屏半屏的元素也不少,如果都作為複合層,交給 GPU,記憶體消耗可想而知,所以一些很極端的硬體加速場景性能非常差:
[caption id="attachment_1251" align="alignnone" width="303"]
gpu compositing issue[/caption]
對於 1GB RAM 的設備,去掉系統和後台進程的 1/3,再去掉瀏覽器和當前頁面的 1/3,實際能用的只有 200 到 300MB,如果複合層太多太大,記憶體會被迅速消耗,然後掉幀(卡頓、閃爍)現象,甚至瀏覽器/應用崩潰也就很合理了
P.S.詳細見 CSS3 硬體加速也有坑!!!
四.創建複合層
瀏覽器在一些情況下會創建複合層,例如:
-
3D transforms: translate3d, translateZ and so on;
-
<video>,<canvas>and<iframe>elements; -
animation of transform and opacity via Element.animate();
-
animation of transform and opacity via СSS transitions and animations;
-
position: fixed;
-
will-change;
-
filter;
-
。。。
還有很多,詳細見 CompositingReasons.h 中定義的常量
這些大多是我們期望的,算是顯式創建的複合層,而另一些情況也會創建複合層:
- 位於複合層之上的元素會被創建複合層(B 的
z-index大於 A,對 A 做動畫,B 也會被塞進獨立的複合層)
很容易理解,A 在動畫過程中可能會與 B 產生重疊,被 B 遮住,那麼 GPU 需要每幀對 A 圖層做動畫,然後再與 B 圖層複合,才能得到正確結果,所以 B 無論如何都要被塞進複合層,連同 A 一起交給 GPU
隱式創建複合層主要出於重疊考慮,如果瀏覽器不確定會不會發生重疊,那麼就要把不確定的東西都塞進複合層,所以,從這個角度看,高 z-index 原則是有道理的
五.硬體加速的優缺點
優點
-
動畫非常流暢,能達到 60fps
-
動畫執行過程在獨立線程裡,不受計算密集的 JS 任務影響
缺點
-
把元素塞進複合層時需要額外重繪,有時很慢(可能需要整頁重繪)
-
複合層數據傳遞給 GPU 有額外時耗,取決於複合層的數量和大小,這在中低端設備可能導致閃爍
-
每個複合層都要消耗一部分記憶體,移動設備上記憶體很貴,過多佔用會導致瀏覽器/應用崩潰
-
存在隱式複合層的問題,不注意的話記憶體飆升
-
文字模糊,元素有時會變形
最主要的問題集中在記憶體消耗和repaint上,所以動畫性能優化目標是降低記憶體消耗,減少 repaint
六.性能優化技巧
1.盡量避免隱式複合層
複合層直接影響 repaint、記憶體消耗:動畫開始時創建複合層、結束時刪除複合層,都會引起 repaint,而動畫開始時必須把圖層數據發送給 GPU,記憶體消耗集中在這裡。兩條建議:
-
給動畫元素應用高
z-index,最好直接作為body的子元素,對於嵌套很深的動畫元素,可以複製一個到body下,僅用於實現動畫效果 -
給動畫元素應用
will-change,瀏覽器會提前把這些元素塞進複合層,可以讓動畫開始/結束時更流暢些,但不能濫用,在不需要的時候趕緊去掉,減少記憶體消耗
2.只改變 transform 和 opacity
能用 transform、opacity 優先用,不能用的話想辦法用,比如背景色漸變,可以用蓋在上面的偽元素背景色 opacity 動畫模擬;box-shadow 動畫可以用鋪在下面的偽元素 opacity 動畫模擬,這些曲折的實現方式能帶來顯著性能提升
3.減少複合層的大小
小元素放大展示,減小 width、height,減少傳遞給 GPU 的數據,由 GPU 做 scale 放大展示,視覺效果無差異(多用於純色背景元素,對不太重要的圖片也可以進行 5% 到 10% 的寬高壓縮),例如:
<div id="a"></div>
<div id="b"></div>
<style>
#a, #b {
will-change: transform;
background-color: #f00;
}
#a {
width: 100px;
height: 100px;
}
#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>
最終顯示的兩個紅色塊在視覺上沒有差異,但減小了 90% 的記憶體消耗
4.考慮對子元素動畫與容器動畫
容器動畫可能存在不必要的記憶體消耗,比如子元素之間的空隙,也會被當做有效數據發送給 GPU,如果對各個子元素分別應用動畫,就能避免這部分的記憶體消耗
例如 12 道陽光光線旋轉,轉容器就把容器整張圖都發送給 GPU,單獨轉 12 道光線就去掉了光線之間的 11 條空隙,能夠節省一半記憶體
5.早早關注複合層的數量和大小
從一開始就關注複合層,尤其是隱式創建的複合層,避免後期優化影響佈局
複合層的大小比數量影響更大,但瀏覽器會做一些優化操作,把幾個複合層整合成一個,叫Layer Squashing,但有時一個大複合層比幾個小複合層消耗的記憶體更多,有必要的話可以手動去掉這種優化:
// 給每個元素應用不同的 translateZ
translateZ(0.0001px), translateZ(0.0002px)
6.不要濫用硬體加速
沒事不要亂加 transform: translateZ(0)、will-change: transform 等強制開啟硬體加速的屬性,GPU 複合存在缺點和不足,而且是非標準的行為,最好情況能帶來顯著性能提升,最壞情況可能會讓瀏覽器崩潰
暫無評論,快來發表你的看法吧