11. web | GPU 動(dòng)畫:正確的打開(kāi)方式

譯者序:原文GPU Animation: Doing It Right,發(fā)表于2016年12月6日,本文是對(duì)該篇的中文翻譯,如有幫助,作為譯者,也深感欣慰。
附原文鏈接:https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/

目前,大部分人都知道現(xiàn)代瀏覽器是使用GPU來(lái)渲染web的部分頁(yè)面,尤其是帶有動(dòng)畫的。舉個(gè)例子,一個(gè)使用transform的css動(dòng)畫看起來(lái)會(huì)比使用lefttop屬性的更為流暢。但是如果你問(wèn),“我是如何從GPU獲得平滑的動(dòng)畫?”多數(shù)情況下,你可能會(huì)聽(tīng)到比如“使用 transform: translateZ(0) 或者 will-change: transform?!钡幕卮稹?/strong>

這些屬性好比如我們?cè)贗E6使用zoom:1(如果你懂我的意思),用于準(zhǔn)備GPU的動(dòng)畫——或者合成(compositing),瀏覽器供應(yīng)商喜歡這么稱它。

但有時(shí),簡(jiǎn)單演示中運(yùn)行的很好很流暢的動(dòng)畫,在真實(shí)網(wǎng)站卻很慢,引起視覺(jué)錯(cuò)誤甚至導(dǎo)致瀏覽器崩潰。為什么會(huì)產(chǎn)生這種現(xiàn)象?我們?nèi)绾涡迯?fù)它?接下來(lái)一起試著理解吧。

免責(zé)聲明

在我們深入GPU的合成前,我想告訴你一件重要的事:這是一個(gè)巨大的hack。你不會(huì)在W3C的規(guī)范里(至少目前來(lái)說(shuō))找到任何關(guān)于合成(compositing )如何工作的資料,如何顯式地在合成層上放置元素,甚至于合成本身。它只是瀏覽器用于執(zhí)行確定任務(wù)的優(yōu)化,并且每個(gè)瀏覽器供應(yīng)商以自己的方式實(shí)現(xiàn)。

你在這篇文章學(xué)到的一切,不是官方說(shuō)明文檔,而是我個(gè)人實(shí)驗(yàn)的結(jié)果,夾雜著一點(diǎn)常識(shí)和不同瀏覽器子系統(tǒng)運(yùn)行原理的知識(shí)。部分可能絕對(duì)是錯(cuò)的,部分可能隨著時(shí)間而變化——這個(gè)要事先說(shuō)明!

合成(Compositing )的工作原理

為了準(zhǔn)備GPU動(dòng)畫的頁(yè)面,我們需要理解瀏覽器的工作原理,而不僅僅是聽(tīng)取來(lái)自網(wǎng)上或本文的隨意建議。

比如說(shuō)一個(gè)頁(yè)面有 AB的元素,均為絕對(duì)定位position: absolute,帶著不同的 z-index。瀏覽器將會(huì)從CPU繪制,然后把生成的圖像發(fā)送給GPU——于屏幕上顯示結(jié)果。

<style>
#a, #b { position: absolute; }
#a {
  left: 30px; 
  top: 30px; 
  z-index: 2;
}
#b { z-index: 1; }
</style>
<div id="a">A</div>
<div id="b">B</div>
圖1

現(xiàn)在用left屬性和css的animation,來(lái)移動(dòng)A元素:

<style>
#a, #b { position: absolute; }
#a {
  left: 30px; 
  top: 30px; 
  z-index: 2;
  animation: move 1s linear;
}
#b { z-index: 1; }
@keyframes move { 
  from { left: 30px; } 
  to { left: 100px; }
}
</style>
<div id="a">A</div>
<div id="b">B</div>
圖2

在這種情況下,對(duì)于每個(gè)動(dòng)畫幀,瀏覽器都會(huì)重新計(jì)算元素的幾何形狀(即回流reflow),渲染頁(yè)面新?tīng)顟B(tài)的圖像 (即重繪repaint),然后再次將其發(fā)給GPU以顯示在屏幕。我們知道重繪是很耗性能成本的,每個(gè)現(xiàn)代瀏覽器都足夠快速的來(lái)重繪頁(yè)面改變的部分,而不是整個(gè)頁(yè)面。瀏覽器在多數(shù)情況下能都很快地重繪,但我們的動(dòng)畫依舊不平滑。

在動(dòng)畫的每一步(甚至遞增)進(jìn)行回流和重繪整個(gè)頁(yè)面,聽(tīng)起來(lái)真的很慢,特別是對(duì)于一個(gè)龐大復(fù)雜的布局。而繪制兩個(gè)獨(dú)立的圖像會(huì)更有效——一個(gè)是A元素,一個(gè)是沒(méi)有A元素的整個(gè)頁(yè)面——然后簡(jiǎn)單的相對(duì)彼此偏移那些圖像。換句話來(lái)說(shuō),合成(composing)緩存的元素圖像會(huì)更快。這也是GPU閃光的地方:它能快速合成帶有亞像素精度的圖像,為動(dòng)畫添加“性感“的平滑度。

為了優(yōu)化合成,瀏覽器得確保css的動(dòng)畫屬性:

  • 不影響文檔流,
  • 不依賴于文檔流,
  • 不會(huì)造成重繪。

有人會(huì)認(rèn)為帶有position: absolute以及fixedtopleft屬性,不依賴于其環(huán)境,但事實(shí)并非如此。比如說(shuō),值為百分比的left屬性,會(huì)取決于.offsetParent的大??;同樣,em, vh以及其他單位也會(huì)取決于自身環(huán)境。而transformopacity是css唯一會(huì)滿足上述情況的屬性。
下面用transform代替left來(lái)動(dòng)畫:

<style>
#a, #b { position: absolute; }
#a {
  left: 30px; 
  top: 30px; 
  z-index: 2;
  animation: move 1s linear;
}
#b { z-index: 1; }
@keyframes move { 
  from { transform: translateX(0); }
  to { transform: translateX(70px); }
}
</style>
<div id="a">A</div>
<div id="b">B</div>

這里,我們聲明式地描述了動(dòng)畫:它的開(kāi)始位置,結(jié)束位置,持續(xù)時(shí)間等。它將提前告訴瀏覽器css的更新屬性。因?yàn)闉g覽器如果知道沒(méi)有任何屬性會(huì)導(dǎo)致回流或重繪,它可以應(yīng)用合成優(yōu)化:繪制合成層(compositing layers)的圖像并發(fā)給GPU。

這種優(yōu)化的優(yōu)點(diǎn)在哪?

  • 得到一個(gè)帶有亞像素精密度的柔順平滑動(dòng)畫,運(yùn)行在特別為圖形任務(wù)的優(yōu)化的單元上,并且非常快。
  • 動(dòng)畫不再綁定到CPU。即使運(yùn)行一個(gè)強(qiáng)化的JavaScript任務(wù),動(dòng)畫依然會(huì)快速執(zhí)行。

一切看起來(lái)如此的清楚和簡(jiǎn)單,對(duì)吧,那會(huì)遇到什么問(wèn)題呢?一起來(lái)看看這種優(yōu)化方式是如何工作的。

它可能會(huì)讓你震驚,GPU竟是一個(gè)獨(dú)立的計(jì)算機(jī)。是的,每個(gè)現(xiàn)代設(shè)備的重要部分通常都是獨(dú)立單元,有自己的處理器,自己的內(nèi)存和數(shù)據(jù)處理模塊。就像其他任何應(yīng)用程序或者游戲一樣,瀏覽器需要用外部設(shè)備跟GPU通信。

為了更好的理解它是怎么工作的,想想AJAX吧。假使你要提交用戶輸入的數(shù)據(jù),你不會(huì)告訴遠(yuǎn)程服務(wù)器,“嗨,過(guò)來(lái)獲取這些輸入框的數(shù)據(jù)和JS變量,并保存到數(shù)據(jù)庫(kù)?!边h(yuǎn)程服務(wù)器不能訪問(wèn)用戶瀏覽器的內(nèi)存。取而代之的是,你需要從頁(yè)面保存這些數(shù)據(jù)到可以輕松解析的簡(jiǎn)單數(shù)據(jù)格式(如JSON)的有效內(nèi)容中,并發(fā)送給遠(yuǎn)程服務(wù)器。

合成也是如此。GPU就像遠(yuǎn)程服務(wù)器,瀏覽器需要首先創(chuàng)建一個(gè)有效載荷,然后發(fā)送到設(shè)備。當(dāng)然,GPU沒(méi)有距離CPU數(shù)千米長(zhǎng);它就在旁邊。然而,鑒于多數(shù)情況,遠(yuǎn)程服務(wù)器請(qǐng)求和返回允許2秒,對(duì)于GPU的數(shù)據(jù)轉(zhuǎn)換的額外3~5毫秒會(huì)導(dǎo)致糟糕的動(dòng)畫。

什么是GPU的有效載荷?多數(shù)情況下,它包含了層圖像,以及附加的說(shuō)明比如大小,偏移量,以及動(dòng)畫參數(shù)。下面大致的寫了有效負(fù)載及GPU傳輸?shù)臄?shù)據(jù):

  • 繪制每個(gè)合成層成獨(dú)立圖像。
  • 準(zhǔn)備層數(shù)據(jù)(例如大小,偏移量,透明度)
  • 準(zhǔn)備動(dòng)畫的著色(如果用到的話)
  • 發(fā)送GPU數(shù)據(jù)

如你所見(jiàn),每當(dāng)為元素添加transform: translateZ(0)或者will-change: transform,你會(huì)開(kāi)始同樣的過(guò)程。而重繪是很耗性能成本的,這里它會(huì)更慢。多數(shù)情況下,瀏覽器不能進(jìn)行遞增的重繪,它會(huì)去繪制之前覆蓋了新合成層的區(qū)域。

Paste_Image.png

隱式合成(Implicit Compositing)

回到我們剛才AB的例子。之前,我們動(dòng)畫處于所有元素上層的A,導(dǎo)致有兩個(gè)合成層:一是A元素,另一個(gè)是B元素和整個(gè)頁(yè)面背景(也就是沒(méi)有A)。
現(xiàn)在,我們讓B動(dòng)畫。

初始狀態(tài)
移動(dòng)狀態(tài)

我們陷入了邏輯問(wèn)題。B元素應(yīng)該是一個(gè)獨(dú)立的合成層,最終的層圖像應(yīng)該在GPU被合成。但是A元素應(yīng)該出現(xiàn)在B的上面,我們并沒(méi)有定義關(guān)于A的任何東西來(lái)推動(dòng)它在自己層。

記住那個(gè)大的聲明:特殊的GPU-合成(GPU-compositing)模式并不是CSS規(guī)范的一部分;它只是瀏覽器內(nèi)部應(yīng)用的優(yōu)化。因?yàn)槎x了z-indexA肯定是在B上方。而瀏覽器會(huì)做些什么呢?

它將會(huì)強(qiáng)制創(chuàng)建一個(gè)包含A的新合成層,當(dāng)然,添加了另一個(gè)重繪:

圖例

它被稱為隱式合成 implicit compositing:以堆疊順序應(yīng)當(dāng)出現(xiàn)在合成上的一個(gè)或多個(gè)非合成元素被提升為復(fù)合層 —— 即,被繪制為分離的圖像,然后將其發(fā)送到GPU。

我們?cè)陔[式合成里犯的錯(cuò)遠(yuǎn)比你想象的還要多。瀏覽器提升元素為合成層是有很多原因的,下面列了幾條:

  • 3D變換: translate3d, translateZ等;
  • <video>,<iframe>元素;
  • 通過(guò)Element.animate()來(lái)改變transform, opacity
  • 通過(guò)css的transitions和animations改變transform, opacity;
  • position: fixed;
  • will-change
  • filter

可以看“CompositingReasons.h” 的文章,有更多關(guān)于谷歌瀏覽器的解釋。

看起來(lái)GPU動(dòng)畫的主要問(wèn)題似乎是意想不到的重繪,事實(shí)上并不是,最大的問(wèn)題是……

內(nèi)存消耗

再一次溫馨提醒,GPU是獨(dú)立式計(jì)算機(jī):它不僅要將渲染的層圖像發(fā)送給GPU,而且要存儲(chǔ)它們便于在以后動(dòng)畫的重用。

那么單個(gè)合成層需要多少內(nèi)存?舉個(gè)例子,猜想下,保存一個(gè)填充色為#FF0000的320*240的矩形,需多少內(nèi)存。

Paste_Image.png

典型的web開(kāi)發(fā)者會(huì)去想,“這是一個(gè)純色圖。我會(huì)把它作為png來(lái)保存,再檢查大小,應(yīng)該比1KB小?!焙翢o(wú)疑問(wèn),他們是正確的,這種圖片作為png是104字節(jié)(byte)。

問(wèn)題是PNG,或JPEG,GIF等用來(lái)存儲(chǔ)以及傳輸圖像數(shù)據(jù)。為了將圖像繪制到顯示器上,計(jì)算機(jī)需要分析圖像格式,然后表示為像素?cái)?shù)組/矩陣。所以,我們的示例圖片將會(huì)占320 × 240 × 3 = 230,400 bytes的計(jì)算機(jī)內(nèi)存。也就是說(shuō),我們要將圖像的寬乘高來(lái)獲取圖片的像素?cái)?shù)。然后,我們?cè)俪?,因?yàn)槊總€(gè)像素由3個(gè)字節(jié)(RGB)描述。如果圖像包含透明區(qū)域,我們需要乘4,因?yàn)樾枰~外的字節(jié)來(lái)描述透明度:(RGBa):320 × 240 × 4 = 307,200 bytes。

瀏覽器總是將合成層繪制為RGBa圖像,看起來(lái)似乎沒(méi)有有效的方法來(lái)判斷元素是否包含透明區(qū)域。

舉個(gè)更可能的例子:10張圖片的輪播效果,每張800*600像素。我們需要在用戶交互(如拖動(dòng))時(shí)讓圖片之間進(jìn)行平滑的切換,因此我們?yōu)槊總€(gè)圖片添加了will-change: transform。這會(huì)事先將圖片提升為合成層,以便在用戶交互時(shí)立即轉(zhuǎn)換。如此一來(lái),計(jì)算機(jī)顯示輪播圖需要的內(nèi)存是: 800 × 600 × 4 × 10 ≈ 19 MB。

19MB的額外內(nèi)存被用來(lái)渲染單個(gè)控件!如果你是現(xiàn)代web開(kāi)發(fā)者,正在創(chuàng)建單頁(yè)面網(wǎng)站,并有很多動(dòng)畫控件、視差效果、高分辨率圖像以及其他視覺(jué)增強(qiáng),那么每頁(yè)額外的100~200MB才剛開(kāi)始。添加隱式合成到混合(承認(rèn)吧——你以前從沒(méi)想過(guò)這個(gè)),那你將會(huì)結(jié)束掉設(shè)備的所有可用內(nèi)存。

此外,多數(shù)情況下,這些顯示相同結(jié)果的內(nèi)存將會(huì)被浪費(fèi)。

圖例

這對(duì)桌面客戶端來(lái)說(shuō)可能不是一個(gè)問(wèn)題,但它真的會(huì)損害移動(dòng)端用戶。首先,現(xiàn)代的很多設(shè)備有高密度屏幕:合成層圖像的權(quán)重要乘4~9。其次,移動(dòng)設(shè)備并沒(méi)有臺(tái)式機(jī)那么大的內(nèi)存。例如,現(xiàn)在的iphone 6有1GB的共有內(nèi)存(即內(nèi)存既用于RAM,也用于VRAM)。考慮到至少1/3的內(nèi)存被用于操作系統(tǒng)和后臺(tái)進(jìn)程,另1/3被用于瀏覽器和現(xiàn)在的頁(yè)面(對(duì)于高度優(yōu)化的沒(méi)有大量框架的頁(yè)面),我們最后會(huì)有大約200~300MB留給GPU效果。而iphone 6 是相當(dāng)昂貴的高端設(shè)備;很多手機(jī)的內(nèi)存會(huì)更少。

你可能會(huì)問(wèn),“在GPU存儲(chǔ)PNG圖片來(lái)減少內(nèi)存空間可能嗎?”技術(shù)上說(shuō),有可能。問(wèn)題是GPU是逐像素地繪制屏幕,意味著要將完整的PNG圖像解碼成一個(gè)一個(gè)的像素。我懷疑這種情況下的動(dòng)畫比每秒1幀更快。

值得一提的是,針對(duì)GPU的 圖像壓縮格式是存在的,但是在壓縮比方面不如PNG或JPEG,而且功能會(huì)受硬件的影響。

優(yōu)缺點(diǎn)

現(xiàn)在我們學(xué)到一些GPU動(dòng)畫的基礎(chǔ),一起總結(jié)下它的優(yōu)缺點(diǎn)吧。

優(yōu)點(diǎn):

  • 這種動(dòng)畫更快更平滑,達(dá)到每秒60幀。
  • 正確制作的動(dòng)畫在單獨(dú)的線程運(yùn)行,不會(huì)被JS的計(jì)算所阻塞。
  • 3D轉(zhuǎn)換比較“廉價(jià)”。

缺點(diǎn)

  • 額外的重繪將元素提升至合成層。有時(shí)候這是很慢的(即我們獲取整個(gè)層的重繪,而不是增量的部分)。
  • 繪制層必須傳輸給GPU。根據(jù)層的數(shù)量和尺寸,傳輸可能會(huì)很慢,而導(dǎo)致中低端的設(shè)備產(chǎn)生閃爍現(xiàn)象。
  • 每個(gè)合成層消耗額外的內(nèi)存。而內(nèi)存是移動(dòng)端的寶貴資源,過(guò)度的內(nèi)存使用會(huì)造成瀏覽器的崩潰
  • 如果不考慮隱式合成,緩慢地重繪,極有可能發(fā)生額外的的內(nèi)存使用和瀏覽器崩潰。
  • 我們會(huì)看到視覺(jué)失真,比如某些情況下Safari里渲染的文字和頁(yè)面內(nèi)容消失或被扭曲。

如你所見(jiàn),GPU動(dòng)畫雖然有著實(shí)用獨(dú)特的優(yōu)點(diǎn),但也有不好的問(wèn)題。其中最重要的是重繪和過(guò)度的內(nèi)存使用;而下面涵蓋的所有優(yōu)化技術(shù)將解決這些問(wèn)題。

瀏覽器設(shè)置

在優(yōu)化前,我們需要了解那些能幫助檢查頁(yè)面合成層,以及提供有關(guān)優(yōu)化效果的明確反饋的工具。

SAFARI

Safari的web檢查器(Web Inspector)有個(gè)“l(fā)ayers”邊欄,來(lái)顯示所有合成層及內(nèi)存消耗,合成原因。來(lái)看這個(gè)邊欄:

  1. 在Safari中,利用? + ? + I打開(kāi)web檢查器,如果沒(méi)用,選擇左上角的“preferences”——> “Advanced” ,勾選“Show Develop Menu in menu bar”選項(xiàng),然后重試。
  2. web檢查器打開(kāi)后,選擇“Elements”選項(xiàng),并在右側(cè)邊欄選擇“Layers”。
  3. 現(xiàn)在點(diǎn)擊“Elements”主面板的DOM節(jié)點(diǎn),你將會(huì)看到選中元素的layers信息(如果它用了合成)以及派生的層。
  4. 單擊派生層查看合成原因。瀏覽器將告訴你為什么將該元素移動(dòng)到自己的合成層。

圖例

(查看大圖)

CHROME

chrome的DevTools有類似的面板,但要先啟動(dòng)標(biāo)記:

  1. 在chrome中,前往chrome://flags/#enable-devtools-experiments,啟動(dòng) “Developer Tools experiments”(開(kāi)發(fā)者工具實(shí)驗(yàn)性功能) 的標(biāo)記。
  2. Mac利用? + ? + I打開(kāi)DevTools,PC利用Ctrl + Shift + I,后點(diǎn)擊右上角的如下圖標(biāo),選擇“Settings”選項(xiàng)。
  3. 轉(zhuǎn)入“Experiments” 面板,勾選 “Layers”選項(xiàng)。
  4. 重新打開(kāi)DevTools,你將看到“Layers”面板。
PC
PC

Mac

(查看大圖)

該面板將當(dāng)前頁(yè)面的所有活動(dòng)合成層顯示為樹(shù)。選擇某個(gè)層,你會(huì)看到相關(guān)信息如大小(size),內(nèi)存消耗(memory consumption),重繪次數(shù)(repaint count)以及合成原因(reason for being composited)。

優(yōu)化建議

已經(jīng)設(shè)置好環(huán)境后,我們開(kāi)始優(yōu)化合成層。之前確定合成的主要兩個(gè)問(wèn)題:額外的重繪(造成GPU的數(shù)據(jù)傳輸問(wèn)題),以及額外的內(nèi)存消耗。因此,下面的所有優(yōu)化建議將針對(duì)上述問(wèn)題:

避免隱式合成

這是最簡(jiǎn)單也最重要的建議,是的,很重要。再次提醒,所有非合成的DOM元素帶有顯示合成原因(如position: fixed, video,css animation)將會(huì)被強(qiáng)制提升為自己層,便于GPU的最終圖像合成。在移動(dòng)端,這可能會(huì)導(dǎo)致動(dòng)畫非常緩慢。

舉個(gè)例子(查看代碼鏈接,戳此進(jìn)):

html
css

A元素要在用戶交互時(shí)進(jìn)行動(dòng)畫。如果在“Layers”面板看這頁(yè)面,你會(huì)看到,它并沒(méi)有多余的層。而點(diǎn)擊“play”按鈕后,你會(huì)看到多層,這些圖層在動(dòng)畫完成后立即刪除。如果在“TimeLine”面板看該過(guò)程,你會(huì)看到動(dòng)畫的開(kāi)始和結(jié)束都伴隨著大面積的重繪。

圖例.png

(查看大圖)

瀏覽器是這么一步步做的:

  1. 當(dāng)頁(yè)面加載好后,瀏覽器若找不到合成原因,它會(huì)選取最佳策略:在單個(gè)背景層上繪制頁(yè)面所有內(nèi)容。
  2. 當(dāng)點(diǎn)擊“play”按鈕時(shí),我們顯然看到增加了元素A的合成——因?yàn)?code>transform屬性。當(dāng)瀏覽器確定堆疊順序的元素A是在元素B的下面,所以它提升B為自己的合成層(隱式合成)。
  3. 提升至合成層總會(huì)造成重繪:瀏覽器必須為元素創(chuàng)建新的紋理,并將其從之前的層刪除。
  4. 新的圖層必須傳輸給GPU,便于用戶在屏幕上看到最終的圖像合成。根據(jù)層數(shù),紋理大小和內(nèi)容復(fù)雜度,需大量時(shí)間來(lái)執(zhí)行重繪和數(shù)據(jù)傳輸。這也就是為什么我們有時(shí)會(huì)看到動(dòng)畫開(kāi)始或結(jié)束時(shí)元素在閃爍。
  5. 動(dòng)畫完成后,我們?nèi)コ嗽?code>A合成的原因,那么,瀏覽器看到已經(jīng)不需要合成了,就會(huì)回退到最佳策略:頁(yè)面所有內(nèi)容都在一個(gè)層,這也就意味著背景層需重新繪制AB(另一個(gè)重繪),并將新的紋理發(fā)給GPU。上述的步驟也就導(dǎo)致了閃爍。

為了擺脫隱式合成問(wèn)題和減少視覺(jué)差異,我建議以下方法:

  • 試著用z-index將動(dòng)畫的元素保持盡可能高。理論上,這些元素應(yīng)該是body的直接子元素。當(dāng)然,當(dāng)動(dòng)畫元素嵌套在DOM樹(shù)內(nèi)且依賴正常流,這標(biāo)記并不總是可能的。這種情況下,你可以克隆元素并將其放在body中僅用于動(dòng)畫。
  • 你可以給瀏覽器一個(gè)wiil-change的提示,表示準(zhǔn)備合成。設(shè)置元素該屬性,瀏覽器將(但不總是)提前將其提升至合成層,以便動(dòng)畫平滑的開(kāi)始結(jié)束。但不要濫用該屬性,否則內(nèi)存將大大增加!
動(dòng)畫用TRANSFORMOPACITY屬性

transformopacity屬性保證既不影響正常流,也不影響DOM環(huán)境(即,不會(huì)造成回流或重繪,動(dòng)畫可以完全轉(zhuǎn)移到GPU)?;旧希@意味著你可以有效的處理動(dòng)畫移動(dòng),縮放,旋轉(zhuǎn),透明度,以及變換。有時(shí)你可能想用這些屬性模仿其他動(dòng)畫類型。

舉個(gè)簡(jiǎn)單的例子:背景顏色的過(guò)渡?;痉椒ㄊ翘砑?code>transition屬性。

<div id="bg-change"></div>
<style>
#bg-change { 
width: 100px; 
height: 100px; 
background: red; 
transition: background 0.4s;
}
#bg-change:hover { background: blue;}
</style>

這種情況下,動(dòng)畫完全在CPU上運(yùn)行,每一步都會(huì)重繪。而我們可以在GPU上實(shí)現(xiàn)同樣的效果:取代background-color屬性,我們?cè)陧敳刻砑右粋€(gè)層來(lái)變化它的opacity:

<div id="bg-change"></div>
<style>
#bg-change { 
width: 100px; 
height: 100px; 
background: red;
}
#bg-change::before { 
background: blue; 
opacity: 0; 
transition: opacity 0.4s;
}
#bg-change:hover::before { opacity: 1; }
</style>

這個(gè)動(dòng)畫更快更平滑,但是記住它會(huì)導(dǎo)致隱式合成和需要額外內(nèi)存。這種情況大大減少內(nèi)存消耗。

減少合成層的大小

看下面的圖片,有發(fā)現(xiàn)不同么?

圖例.png

這兩個(gè)合成層視覺(jué)上是一樣的,但第一個(gè)40000字節(jié)(39KB),第二個(gè)才400字節(jié),少100倍。為什么?看代碼:

<div id="a"></div>
<div id="b"></div>
<style>
#a, #b { will-change: transform;}
#a { width: 100px; height: 100px;}
#b { width: 10px; height: 10px; transform: scale(10);}</style>

不同點(diǎn)在于#a的物理尺寸是100*100像素(100*100*4=40000字節(jié)),而#b只有10*10像素大?。?0*10*4=400字節(jié)),利用transform: scale(10)放大成100*100像素。#b是合成層,由于帶了will-change屬性,在最終圖像繪制期間,transform完全是在GPU上發(fā)生。

技巧很簡(jiǎn)單:利用widthheight屬性減少物理大小,利用transform: scale(…)升級(jí)其紋理。當(dāng)然,對(duì)于非常簡(jiǎn)單的純色層來(lái)說(shuō),這個(gè)技巧極大地減少了內(nèi)存的消耗。舉個(gè)例子,如果你想動(dòng)畫一張大照片,你可以縮小它到5%到10%,然后放大它;用戶可能看不出任何差別,你也節(jié)省出幾兆的寶貴內(nèi)存。

如果可以的話,利用CSS的transitions和animations

我們已經(jīng)知道,通過(guò)transform以及opacity會(huì)自動(dòng)創(chuàng)建合成層,并在GPU上運(yùn)行。我們同樣可以通過(guò)JavaScript來(lái)動(dòng)畫,但需要添加transform: translateZ(0)will-change: transform, opacity來(lái)保證元素獲得自己的合成層。

requestAnimationFrame回調(diào)計(jì)算每個(gè)步驟,發(fā)生JavaScript動(dòng)畫,通過(guò)Element.animate()
是一個(gè)有效的css動(dòng)畫聲明。

一方面,通過(guò)css的transition或animation來(lái)創(chuàng)建簡(jiǎn)單重用的動(dòng)畫是很容易的;而另一方面,JS創(chuàng)建復(fù)雜的動(dòng)畫比css簡(jiǎn)單的多。此外,JavaScript是與用戶交互的唯一的路徑。

哪種方式更好?我們可以利用通用JavaScript庫(kù)來(lái)動(dòng)畫元素么?

基于CSS的動(dòng)畫有個(gè)重要的特征:它是完全在GPU上運(yùn)行的。因?yàn)槟?strong>聲明了動(dòng)畫應(yīng)該怎么開(kāi)始和結(jié)束,瀏覽器可以在動(dòng)畫開(kāi)始前準(zhǔn)備所有命令,并發(fā)給GPU。在命令式JavaScript的情況下,瀏覽器需要當(dāng)前所有幀的狀態(tài)。為了實(shí)現(xiàn)平滑的動(dòng)畫,我們需要在主瀏覽器線程計(jì)算新幀,然后每秒發(fā)送給GPU至少60次。除了計(jì)算和發(fā)送數(shù)據(jù)比css慢的多的事實(shí)外,它們還依賴于主進(jìn)程的工作負(fù)載。

圖例.png

上述的范例里,你可以當(dāng)主進(jìn)程會(huì)被強(qiáng)化的JavaScript計(jì)算阻塞時(shí)會(huì)發(fā)生什么,而css動(dòng)畫不會(huì)被影響,因?yàn)樾聨窃趩为?dú)的線程里計(jì)算的,但JavaScript的動(dòng)畫需要等大量計(jì)算完成后才計(jì)算新幀。

因此,盡可能使用基于css的動(dòng)畫,特別是加載和進(jìn)度指示,不僅更快,而且不會(huì)被大量JavaScript計(jì)算所阻塞。

現(xiàn)實(shí)的優(yōu)化實(shí)例

這篇文章是我在開(kāi)發(fā) Chaos Fighters頁(yè)面過(guò)程中調(diào)查和試驗(yàn)的結(jié)果。這是一個(gè)有著很多動(dòng)畫的手機(jī)游戲的響應(yīng)推廣頁(yè)面。當(dāng)開(kāi)始開(kāi)發(fā)時(shí),我只知道如何產(chǎn)生基于GPU的動(dòng)畫,但并不知道它的工作原理。結(jié)果,第一個(gè)里程碑頁(yè)面導(dǎo)致iphone5 —— 當(dāng)時(shí)最新的Apple手機(jī)——在加載完頁(yè)面后幾秒內(nèi)崩潰。而現(xiàn)在,即使是不太高級(jí)的手機(jī),這個(gè)頁(yè)面依然正常運(yùn)行。

一起考慮這個(gè)頁(yè)面的有趣優(yōu)化。

頁(yè)面最頂部是游戲介紹,類似紅色的光線在背景中旋轉(zhuǎn)。毫無(wú)疑問(wèn)是個(gè)無(wú)限循環(huán),沒(méi)有交互,一個(gè)很好的css動(dòng)畫范例。第一個(gè)(誤導(dǎo))的嘗試是保存光線圖像作為img元素置于頁(yè)面上,并使用無(wú)限的css動(dòng)畫。鏈接:[http://codepen.io/sergeche/pen/gwBjqG]

Paste_Image.png

一切都看起來(lái)很正常,但是光線的圖片是很大的,移動(dòng)端用戶并不會(huì)高興。

仔細(xì)觀察圖像。基本上,它只是來(lái)自圖像中心的幾條光線,而光線是相同的,所以我們可以保存單個(gè)光線圖像,并反復(fù)利用達(dá)到最終效果。最終得到單光線圖像,這遠(yuǎn)比初始圖小的多。

針對(duì)這種優(yōu)化,我們必須將.sun的標(biāo)記復(fù)雜化,它是光線圖像元素的容器。每一光線都有特定的旋轉(zhuǎn)角度。(代碼鏈接)[http://codepen.io/sergeche/pen/qaJraq]

圖例.png

視覺(jué)效果一樣,但網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量會(huì)少很多。合成層的尺寸都為500×500×4≈977KB。

弄的簡(jiǎn)單些,示例的光線圖片很小,只有500*500像素。在真實(shí)的網(wǎng)站,設(shè)備的大小及像素分辨率并不相同(手機(jī),平板,電腦),最終的圖片大約是3000*3000*4=36MB!而這僅僅是頁(yè)面上的一個(gè)動(dòng)畫元素。

再看下“Layers”面板的頁(yè)面元素。我們已經(jīng)讓整個(gè)太陽(yáng)旋轉(zhuǎn)變得簡(jiǎn)單。因此,這個(gè)容器會(huì)被提升至合成層,被繪制成單一的大紋理圖像,然后發(fā)給CPU。正因?yàn)槲覀兊暮?jiǎn)化,紋理中包含了無(wú)用的數(shù)據(jù):之間的縫隙。

更多來(lái)說(shuō),無(wú)用的數(shù)據(jù)比有用的還多!占據(jù)有限的內(nèi)存資源并不是一個(gè)最好的方式。

這個(gè)問(wèn)題的解決方案跟網(wǎng)絡(luò)傳輸?shù)膬?yōu)化相同:發(fā)送有用的數(shù)據(jù)(即光線)給GPU,我們可以計(jì)算節(jié)約了多大內(nèi)存:

  • 太陽(yáng)容器:500*500*4 = 977KB
  • 12條線: 250*40*4*12 = 469KB

內(nèi)存消耗減少2倍。要做到這一點(diǎn),我們分別動(dòng)畫每條線,替換整個(gè)容器。這樣一來(lái),只有光線圖片會(huì)被發(fā)給GPU,之間的間隙不會(huì)占據(jù)任何資源。

我們不得不使標(biāo)簽復(fù)雜,以便單獨(dú)對(duì)光線進(jìn)行動(dòng)畫處理,而css的干擾也會(huì)更多。我們已經(jīng)對(duì)線條初始旋轉(zhuǎn)動(dòng)畫用了transform,然后開(kāi)始每個(gè)動(dòng)畫一樣的效果,旋轉(zhuǎn)360度?;旧希覀冃枰?jiǎng)?chuàng)建一個(gè)單獨(dú)的@keyframes部分,有很多傳輸?shù)拇a。

編寫一個(gè)簡(jiǎn)短的JavaScript來(lái)處理光線初始放置,并允許對(duì)動(dòng)畫,光線數(shù)量等進(jìn)行微調(diào),這將變得更容易。見(jiàn)代碼 [http://codepen.io/sergeche/pen/bwmxoz]

圖例

新動(dòng)畫看起來(lái)跟之前一樣,但內(nèi)存消耗上少了2倍。

而且,在布局組成上,動(dòng)畫的太陽(yáng)不是主要元素,而是背景元素。光線沒(méi)有清晰的對(duì)比元素。這意味著,我們可以發(fā)略低分辨的光線圖給GPU,隨后將其升級(jí),這幫我們減少一點(diǎn)內(nèi)存消耗。

嘗試將紋理大小減小10%。光線的物理大小是250*0.9*40*0.9=255*36像素。為了使光線看起來(lái)像250*20,我們將其放大250÷225≈1.111。

我們將添加一行代碼background-size: cover.sun-ray,便于背景圖片自動(dòng)調(diào)整,然后添加transform: scale(1.111)給光線。代碼http://codepen.io/sergeche/pen/YGJOva

圖例

注意,我們只改變了元素的大小; PNG圖像的大小保持不變。由DOM元素創(chuàng)建的矩形將作為GPU的紋理,而不是PNG圖像。

太陽(yáng)光線在GPU的合成大小是225×36×4×12≈380 KB(之前是469KB)。我們減少了大概19%的內(nèi)存,并獲得更靈活的代碼,通過(guò)縮減來(lái)得到更佳質(zhì)量——內(nèi)存比。因此,增加簡(jiǎn)單動(dòng)畫的復(fù)雜性,減少了977÷380≈2.5倍的內(nèi)存!

我想你已經(jīng)注意到,這個(gè)解決方案有個(gè)重大的缺點(diǎn):動(dòng)畫運(yùn)行在CPU上會(huì)被JavaScript計(jì)算阻塞。如果你想更熟悉GPU操作動(dòng)畫,我提個(gè)作業(yè)。codepen上fork下Codepen of the sun rays,使其完全運(yùn)行在GPU上,就像先前的例子一樣高效靈活。在評(píng)論中發(fā)布你的代碼以獲得反饋。

收獲

對(duì)于Chaos Fighters 頁(yè)面的優(yōu)化使我重新思考開(kāi)發(fā)現(xiàn)代網(wǎng)頁(yè)的過(guò)程。這里列了幾條我的主要原則:

  • 始終與客戶,設(shè)計(jì)談?wù)摼W(wǎng)站上的所有動(dòng)畫和效果。這會(huì)大大影響頁(yè)面的標(biāo)簽,以為更好的合成。
  • 一開(kāi)始注意合成層的數(shù)量和大小,特別是隱式合成層。瀏覽器的開(kāi)發(fā)工具中的“Layers”面板是你最好的伙伴。
  • 現(xiàn)代瀏覽器頻繁使用合成,不僅用于動(dòng)畫,而且優(yōu)化頁(yè)面元素繪制。舉個(gè)例子,position: fixediframe,video使用合成。
  • 合成層的大小比數(shù)量更重要。某些情況下,瀏覽器會(huì)嘗試減少合成層的數(shù)量(查看“GPU Accelerated Compositing in Chrome“的 “Layer Squashing”這一塊),它防止了“層爆炸”并減少內(nèi)存消耗,特別是層有巨大的交叉點(diǎn)時(shí)。有時(shí)候,這種優(yōu)化具有負(fù)面影響,比如說(shuō)一個(gè)大的紋理比幾個(gè)小的層消耗更多內(nèi)存。為了繞過(guò)這個(gè)優(yōu)化,我給了translateZ()很小的值,比如說(shuō)translateZ(0.0001px),translateZ(0.0002px)。瀏覽器將確定元素位于3D空間的不同面板,因此跳過(guò)優(yōu)化。
  • 你不能僅添加transform: translateZ(0)will-change: transform給任意元素,來(lái)虛擬提高動(dòng)畫性能或擺脫視覺(jué)差。GPU的合成要考慮弊端和取舍。當(dāng)不使用時(shí),合成會(huì)降低整體性能,最壞的情況導(dǎo)致瀏覽器崩潰。

請(qǐng)?jiān)试S我再次提醒:這不是GPU合成的官方規(guī)范,每個(gè)瀏覽器解決同一問(wèn)題方式是不同的。本文某些內(nèi)容在幾個(gè)月后可能就過(guò)時(shí)了。例如,谷歌開(kāi)發(fā)者正在探索如何減少CPU到GPU數(shù)據(jù)傳輸?shù)拈_(kāi)銷,包括零復(fù)制開(kāi)銷的特殊共享內(nèi)存的使用。此外,Safari已經(jīng)能夠?qū)⒑?jiǎn)單元素的繪制(比如說(shuō)有background-color的空DOM元素)委托給GPU,而不是在CPU上創(chuàng)建圖像。

無(wú)論如何,我希望這篇文章能幫助你更好地理解瀏覽器是如何使用GPU渲染的,以便您創(chuàng)建能在各設(shè)備下快速運(yùn)行的令人印象深刻的網(wǎng)站了。

###詞匯介紹

1. 紋理(texture)?

這里的紋理是 GPU 的一個(gè)術(shù)語(yǔ):可以把它想象成一個(gè)從主存儲(chǔ)器(例如 RAM)移動(dòng)到圖像存儲(chǔ)器(例如 GPU 中的 VRAM)的位圖圖像(bitmap image)。一旦它被移動(dòng)到 GPU 中,你可以將它匹配成一個(gè)網(wǎng)格幾何體(mesh geometry),在 Chrome 中使用紋理來(lái)從 GPU 上獲得大塊的頁(yè)面內(nèi)容。[參考源自http://web.jobbole.com/85993/]

2. 回流(reflow)

當(dāng)渲染樹(shù)(render Tree)中的一部分(或全部)因?yàn)樵氐囊?guī)模尺寸,布局,隱藏等改變而需要重新構(gòu)建。這就稱為回流(reflow),也就是重新布局(relayout)。
每個(gè)頁(yè)面至少需要一次回流,就是在頁(yè)面第一次加載的時(shí)候。在回流的時(shí)候,瀏覽器會(huì)使渲染樹(shù)中受到影響的部分失效,并重新構(gòu)造這部分渲染樹(shù),完成回流后,瀏覽器會(huì)重新繪制受影響的部分到屏幕中,該過(guò)程成為重繪。[參考源自http://web.jobbole.com/85993/]

3. 重繪(repaint)

當(dāng)render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀,風(fēng)格,而不會(huì)影響布局的,比如 background-color 。則就叫稱為重繪。
值得注意的是,回流必將引起重繪,而重繪不一定會(huì)引起回流。
明顯,回流的代價(jià)更大,簡(jiǎn)單而言,當(dāng)操作元素會(huì)使元素修改它的大小或位置,那么就會(huì)發(fā)生回流。[參考源自http://web.jobbole.com/85993/]

4. 亞像素精度(subpixel precision)

亞像素精度是指相鄰兩像素之間細(xì)分情況。輸入值通常為二分之一,三分之一或四分之一。這意味著每個(gè)像素將被分為更小的單元從而對(duì)這些更小的單元實(shí)施插值算法。例如,如果選擇四分之一,就相當(dāng)于每個(gè)像素在橫向和縱向上都被當(dāng)作四個(gè)像素來(lái)計(jì)算。因此,如果一張5x5像素的圖像選擇了四分之一的亞像素精度之后,就等于創(chuàng)建了一張20x20的離散點(diǎn)陣,進(jìn)而對(duì)該點(diǎn)陣進(jìn)行插值。[來(lái)自百度百科]

外文原文:
https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/?utm_source=CSS-Weekly&utm_campaign=Issue-243&utm_medium=email#one-big-disclaimer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容