正確使用GPU動畫

最近看到一篇關(guān)于GPU動畫的神文,原文地址:https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/。
特此翻譯出來,供自己以及他人學(xué)習(xí)和查看。轉(zhuǎn)載請標(biāo)明出處,謝謝。
另外,由于簡書和GitHub的markdown均不支持內(nèi)嵌iframe。而文中的大多數(shù)效果圖是嵌在iframe中的,可以移步github,下載后在支持內(nèi)嵌iframe的markdown環(huán)境下查看。

如今,絕大多數(shù)人知道現(xiàn)代瀏覽器采用GPU來渲染網(wǎng)頁的部分內(nèi)容,尤其是動畫部分。比如,采用transform屬性的CSS動畫看起來比使用lefttop屬性的動畫更流暢。但如果你要問:“我如何利用GPU實(shí)現(xiàn)流暢的動畫?”絕大多數(shù)情況下,你會聽到如下回答:“使用transform: translateZ(0)或者will-change: transform?!?/h3>

在開始GPU動畫-或者合成(compositing,瀏覽器廠商喜歡這么稱呼)之前,從某種意義上來說,這些個屬性就有點(diǎn)像IE6中使用的zoom:1一樣。(譯者注:有點(diǎn)拗口,意思應(yīng)該是許多人只知道這樣設(shè)置就行了,但是并不知道具體的原因)

但有時候,簡單demo中絲滑流暢的動畫,在實(shí)際網(wǎng)站中運(yùn)行非常慢,造成視覺假象,甚至讓瀏覽器崩潰。為什么會這樣?如何修復(fù)?讓我們來了解一下。

免責(zé)聲明

在深入研究GPU合成之前,我想告訴你們一件十分重要的事:這是一個大大的hack。至少到目前為止,合成的工作原理,如何明確地將元素置于合成層,或者合成本身,關(guān)于這些問題,你在W3C規(guī)范上找不到任何答案。它只是瀏覽器執(zhí)行特定任務(wù)時的一種優(yōu)化操作,每個瀏覽器廠商有自己的實(shí)現(xiàn)方式。

本篇文章中你學(xué)到的所有東西,并不是合成原理的官方解釋,而是我實(shí)驗的結(jié)果,加上一點(diǎn)對瀏覽器子系統(tǒng)差異的常識和理解。有些東西可能是錯的,有些可能隨著時間而變化——提醒過你了。

合成如何工作

在開始GPU動畫頁面之前,我們得知道瀏覽器是如何工作的,不要簡單地聽從網(wǎng)上的或者本篇文章中的一些隨意的建議。

假設(shè)我們有一個頁面,其中有AB兩個元素,每一個都設(shè)置了position: absolute和不同的z-index值。瀏覽器會用CPU繪制,之后把完成的圖像發(fā)送給GPU,由它顯示在屏幕上。

<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>

<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example1.html" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

我們決定采用left屬性和CSS動畫,讓A元素動起來:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="a">A</div>
<div id="b">B</div>

<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example1.html#.a:anim-left" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

此種情況下,對于每個動畫幀,瀏覽器都必須重新計算元素的位置(即reflow),渲染頁面新狀態(tài)的圖像(即repaint),之后再發(fā)送給GPU顯示到屏幕上。我們知道,重繪是非常消耗性能的,但是每個現(xiàn)代瀏覽器都足夠智能,只重繪頁面中變化的部分,而不是整個頁面。盡管絕大多數(shù)情況下,瀏覽器可以很快地重繪,但我們的動畫仍然不是太流暢。

在動畫的每個階段回流、重繪整個頁面(即便是增量繪制),聽起來就很慢,尤其是又大又復(fù)雜的布局。僅繪制兩個獨(dú)立的圖像可能更高效——一個為A元素,另一個為A元素以外的整個頁面——之后簡單地偏移兩個圖片的相對位置。也就是說,合成緩存元素的圖像可能更快。這就是GPU的優(yōu)勢所在:它能夠以亞像素精度快速合成圖像,使得動畫如絲般順滑。

為了優(yōu)化合成,瀏覽器必須確保添加動畫的CSS屬性:

  • 不會影響文檔流,
  • 不依賴文檔流,
  • 不會造成重繪。

有人可能會以為,topleft屬性,輔之以positionabsolutefixed,不依賴元素的環(huán)境,但其實(shí)并不是這樣。例如,left屬性可能是個百分比值,其依賴于.offsetParent的尺寸;另外,em,vh和其它單位依賴于它們的環(huán)境。相反,transformopacity是僅有的滿足上述條件的CSS屬性。

讓我們用transform而不是left來實(shí)現(xiàn)動畫:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}
</style>
<div id="a">A</div>
<div id="b">B</div>

此處,我們以聲明的方式描述動畫:起始位置,結(jié)束位置,持續(xù)時間等。這等于提前告訴瀏覽器哪些CSS屬性會更新。因為瀏覽器發(fā)現(xiàn)沒有屬性會造成回流或者重繪,它就會采用合成優(yōu)化:畫兩幅圖像作為合成層,之后發(fā)送到GPU。

這種優(yōu)化的優(yōu)點(diǎn)是什么呢?

  • 我們獲得了一個亞像素精度的、如絲般順滑的動畫,運(yùn)行在專門為圖形任務(wù)優(yōu)化的單元上。并且運(yùn)行得非??臁?/li>
  • 動畫再也不受限于CPU。即使運(yùn)行繁重的JavaScript任務(wù),動畫依然很快。

一切聽起來似乎簡單明了,不是嗎?我們會遇到哪些問題?讓我們看看這種優(yōu)化的工作原理。

GPU是一個獨(dú)立的計算機(jī),這可能讓你覺得吃驚。確實(shí)如此:每個現(xiàn)代設(shè)備必不可缺的部分是一個獨(dú)立的單元,它有自己的處理器和內(nèi)存、數(shù)據(jù)處理模塊。如同其它應(yīng)用和游戲一樣,瀏覽器必須與GPU進(jìn)行通信,好像和外設(shè)一樣。

為了更好地理解其工作原理,想像下Ajax。假設(shè)你想用填寫的表單數(shù)據(jù)注冊網(wǎng)站用戶。你不能簡單地告訴遠(yuǎn)端的服務(wù)器,“嗨,把這些表單數(shù)據(jù)和JavaScript變量保存到數(shù)據(jù)庫中?!边h(yuǎn)端數(shù)據(jù)庫無法訪問用戶瀏覽器的內(nèi)存。反而,你必須把頁面中的數(shù)據(jù)以易解析的格式(比如JSON),收集在一個payload中,然后發(fā)給遠(yuǎn)端服務(wù)器。

合成的過程也差不多。GPU就像個遠(yuǎn)端的服務(wù)器,瀏覽器必須先創(chuàng)建一個payload,之后再發(fā)送給GPU。顯然,GPU不是遠(yuǎn)離CPU千里之外;它就在那。在很多情況下,對遠(yuǎn)端服務(wù)器的請求和響應(yīng)間隔時間在2S內(nèi)是可以接受的。而對于GPU,3到5毫秒的延遲卻能導(dǎo)致動畫卡頓。

GPU payload長什么樣?一般由層圖像組成,還有一些附加的說明,比如層的尺寸,偏移,動畫參數(shù)等。以下是GPU的payload生成和傳輸?shù)拇蟾胚^程:

  • 將每個合成層繪制為獨(dú)立的圖像
  • 準(zhǔn)備層數(shù)據(jù)(尺寸,偏移,不透明度等)
  • 為動畫準(zhǔn)備著色器(如果可用的話)
  • 發(fā)送數(shù)據(jù)給GPU

如你所見,每次給元素添加神奇的transform: translateZ(0)或者will-change: transform屬性時,都開啟了同樣的過程。然而重繪是非常耗性能的,此時會變得更慢。多數(shù)情況下,瀏覽器無法增量重繪。它必須用新創(chuàng)建的合成層繪制之前覆蓋的區(qū)域:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/before-after-compositing.html" height="270" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

隱式合成

讓我們回到之前A B元素的例子。早先,我們將A做成動畫,它處在頁面所有元素之上。這會生成兩個合成層:A元素一個,B元素和頁面背景一個。
現(xiàn)在,我們讓B元素動起來:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example3.html#.b:anim-translate" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>
我們遇到了一個邏輯問題。元素B應(yīng)該在一個獨(dú)立的合成層,屏幕最終呈現(xiàn)的圖像應(yīng)該在GPU中合成。但是A元素應(yīng)該出現(xiàn)在B元素上面,并且我們沒有指定A提升到自己的層。

記得之前的免責(zé)聲明:GPU合成模式并不是CSS規(guī)范的一部分;它只是瀏覽器內(nèi)部使用的一種優(yōu)化策略。如z-index定義的那樣,我們強(qiáng)制A出現(xiàn)在B的上面。那么,瀏覽器會怎么做呢?

猜對了!瀏覽器會強(qiáng)制為A創(chuàng)建新的合成層——當(dāng)然,增加了一次繁重的重繪:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example4.html#.b:anim-translate" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

這稱為隱式合成:按照棧順序,一個或多個非合成元素出現(xiàn)在合成元素上面時,會被提升到合成層——即被繪制成獨(dú)立的圖像發(fā)送到GPU中。

我們遇到隱式合成的情況比你想象的要頻繁的多。瀏覽器會因很多原因?qū)⒁粋€元素提升為合成層,比如:

  • 3D 變換:translate3dtranslateZ等等;
  • <video>、<canvas><iframe>元素;
  • 通過Element.animate()實(shí)現(xiàn)的transformopacity動畫;
  • 通過CSS transition animation實(shí)現(xiàn)的transformopacity動畫;
  • position: fixed;
  • will-change;
  • filter;

更多情況請參考Chromium項目的“CompositingReasons.h”文件。

似乎GPU動畫的主要問題是意想不到的大量重繪。但并不是。最大的問題是。。。

內(nèi)存消耗

再一次溫馨提示:GPU是獨(dú)立的計算機(jī):它不僅需要發(fā)送渲染好的圖片給GPU,而且需要對其進(jìn)行存儲,以便后續(xù)動畫復(fù)用。

一個合成層需要消耗多少內(nèi)存?讓我們看個簡單點(diǎn)的例子。猜猜存儲一個320×240像素,填滿#FF0000顏色的長方形需要多少內(nèi)存。
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/rect.html" height="270" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

一個標(biāo)準(zhǔn)的web開發(fā)者這樣想:“嗯,這是個純色的圖像,我會將其保存為PNG然后查看其大小。應(yīng)該小于1KB”。沒錯,這個PNG圖片大概104字節(jié)。

問題是,PNG以及JPEG,GIF等,用來存儲和傳輸圖像數(shù)據(jù)。為了將這樣的圖像繪制到屏幕上,計算機(jī)必須解壓圖像數(shù)據(jù),然后表示成像素數(shù)組。因此,我們的樣圖會消耗320 × 240 × 3 = 230,400 bytes的內(nèi)存。也就是,圖片寬度乘以高度獲得圖片的像素數(shù)。之后再乘以3,因為每個像素由3個字節(jié)描述(RGB)。如果圖片包含透明通道,就得乘以4,因為附加的一個字節(jié)用來描述透明度(RGBa):320 × 240 × 4 = 307,200 bytes。

瀏覽器總是按照RGBa圖像的形式繪制合成層。似乎沒有行之有效的方法來確定圖片是否包含了透明通道。

再看一個更常見的例子:一個有10張圖的旋轉(zhuǎn)盤,每張圖800×600像素。我們希望用戶交互,比如拖拽時,圖片之間能夠平滑過渡,因此,我們?yōu)槊糠鶊D添加will-change: transform。這會提前將圖片提升到合成層,因此,用戶一開始交互時,過渡就會開始?,F(xiàn)在計算下僅僅展示這一旋轉(zhuǎn)盤需要多少額外內(nèi)存:800 × 600 × 4 × 10 ≈ 19 MB

僅僅一個控制點(diǎn)就需要額外19MB內(nèi)存!如果你是一個單頁應(yīng)用的WEB開發(fā)者,頁面中有多個動畫控制點(diǎn),視差效果,高分辨率圖像和其它視覺增強(qiáng)效果,那么每個頁面多增加100到200MB僅僅是個開始。再考慮上隱式合成的話(承認(rèn)吧——你之前根本沒想過這個),最終頁面會耗盡設(shè)備的內(nèi)存。

此外,多數(shù)情況下,這些內(nèi)存會被浪費(fèi)掉,用來顯示同樣的結(jié)果:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example5.html" height="620" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

對于桌面客戶端來說,這可能不是個問題,但會深深刺痛移動用戶的心。首先,絕大多數(shù)現(xiàn)代設(shè)備擁有高分辨率的屏幕:這就將合成層圖片的體量乘以4到9。其次,移動設(shè)備不像桌面設(shè)備那樣有那么大的內(nèi)存。比如,不是太舊的iPhone6僅搭載1GB共享內(nèi)存(即,內(nèi)存同時用于RAM和VRAM)??紤]到至少三分之一的內(nèi)存用于操作系統(tǒng)和后臺進(jìn)程,另外的三分之一用于瀏覽器和當(dāng)前頁面(最好的情況是高度優(yōu)化的頁面,沒有太多的framework),我們至多剩下200到300MB供GPU渲染。并且iPhone6是個相當(dāng)昂貴的高端設(shè)備,更多平價的手機(jī)所搭載的內(nèi)存更少。

你也許會問:“有可能在GPU上存儲PNG圖片來減少內(nèi)存占用嗎?”技術(shù)上是可行的。唯一的問題是GPU在屏幕上是逐像素繪制的,這意味著它必須一次次地解碼整個PNG圖片來獲取每個像素數(shù)據(jù)。我懷疑這種情況下的動畫比每秒一幀快點(diǎn)。

GPU特定的圖像壓縮格式確實(shí)存在,但毫無意義。從壓縮比來看,根本比不上PNG或者JPEG,并且使用上也缺乏硬件支持。

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

既然學(xué)了些GPU動畫的基本原理,讓我們總結(jié)下它的優(yōu)缺點(diǎn):

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

  • 動畫既快又流暢,達(dá)到每秒60幀。
  • 精心制作的動畫在獨(dú)立的線程中運(yùn)行,不會被繁重的JavaScript計算阻塞
  • 3D變換很“廉價”

缺點(diǎn)

  • 需要附加的重繪來將元素提升到合成層。有時這個過程很慢(比如,進(jìn)行全層重繪,而不是增量重繪)。
  • 繪制的層必須傳到GPU中。依據(jù)層的大小和數(shù)量,傳輸可能很慢。這可能導(dǎo)致中低端設(shè)備上元素閃爍。
  • 每個合成層消耗額外的內(nèi)存。在移動設(shè)備上,內(nèi)存是寶貴的資源。內(nèi)存超標(biāo)使用會使瀏覽器崩潰
  • 如果你不考慮隱式合成,重繪緩慢、額外內(nèi)存使用和瀏覽器崩潰的可能性會很高。
  • 我們會看到視覺假象,比如Safari上文本渲染,在某些情況下,頁面內(nèi)容會消失或者混亂。

如你所見,盡管有些獨(dú)特的優(yōu)勢,GPU動畫仍然有些令人討厭的問題。最重要的是重繪和大量的內(nèi)存消耗;因此,以下所有的優(yōu)化策略都是處理這些問題的。

瀏覽器設(shè)置

在開始優(yōu)化之前,我們得學(xué)習(xí)一些工具,來幫助我們檢查頁面的合成層,并且提供合理的優(yōu)化反饋。

Safari

Safari的web 監(jiān)視器有個很棒的“Layers”邊條,它顯示所有的層以及內(nèi)存消耗,以及合成的原因。來看看這個邊條:

  1. 在Safari中,按? + ? + I打開web監(jiān)視器。如果不起作用,打開“Preferences” → “Advanced”,開啟“Show Develop Menu in menu bar”選項,再試一次。
  2. 當(dāng)web監(jiān)視器打開后,選擇“Elements”面板,選擇右邊條的“Layers”。
  3. 現(xiàn)在,當(dāng)你在主“Elements”上點(diǎn)擊一個DOM元素時,你會看到一個關(guān)于選擇元素以及所有后代合成層的信息層(如果使用了合成的話)。
  4. 點(diǎn)擊一個后代層,查看其合成原因。瀏覽器會告訴你為什么決定把這個元素遷移至它自己的合成層。


Chrome

Chrome的開發(fā)者工具欄有個類似的面板,但你必須首先激活它:

  1. 在Chrome中,訪問chrome://flags/#enable-devtools-experiments,之后啟用“Developer Tools experiments”項。
  2. ? + ? + I(Mac)或者Ctrl + Shift + I(PC)打開工具欄,之后點(diǎn)擊右上角的如下圖標(biāo),選擇“Setting”菜單項:
  3. 回到“Experiments”面板,啟用“Layers”面板。
  4. 重新打開開發(fā)者工具欄?,F(xiàn)在,你就能看到“Layers”面板了。


這個面板以樹的形式展示當(dāng)前頁面所有活動的合成層。當(dāng)選擇一個層的時候,你會看到諸如尺寸,內(nèi)存消耗,重繪次數(shù)和合成原因。

優(yōu)化建議

現(xiàn)在我們已經(jīng)設(shè)置好環(huán)境,可以開始優(yōu)化合成層了。我們已經(jīng)確定了合成的兩個主要問題:額外的重繪,也會造成數(shù)據(jù)數(shù)據(jù)傳送到GPU,還有額外的內(nèi)存消耗。因此,以下所有的優(yōu)化建議都是針對這兩個問題的。

避免隱式合成

這是最簡單明了的建議,同樣也十分重要。提醒你一下,處在一個顯式合成層(比如position: fixed,視頻,CSS動畫等)之上的所有非合成DOM元素,會被強(qiáng)制提升到自己的層,僅僅為了最后的GPU圖像合成。在移動設(shè)備上,可能會導(dǎo)致動畫啟動緩慢。

舉個簡單的例子:

<iframe height="305" scrolling="no" src="https://codepen.io/sergeche/embed/jrZZgL/?height=305&theme-id=light&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true" style="width: 100%;"></iframe>

A元素是個需要用戶交互啟動的動畫。如果你在“Layers”面板中查看這個頁面,你會發(fā)現(xiàn)沒有多余的層。但當(dāng)點(diǎn)擊“Play”按鈕后,你會看到更多的層,這些層在動畫結(jié)束后立即被移除。如果看下“Timeline”面板,會發(fā)現(xiàn)動畫的開始和結(jié)束位置充斥大片區(qū)域的重繪:

以下是瀏覽器所做的,一步接一步:

  1. 頁面加載完成后,瀏覽器找不到任何合成的理由,因此選擇了最優(yōu)的策略:在單個背景層上繪制整個頁面內(nèi)容。
  2. 點(diǎn)擊“Play”按鈕,我們給元素A顯式增加了合成層——transfrom屬性的一個變換。但是瀏覽器發(fā)現(xiàn)按照棧順序,元素A在元素B下面,因此也將B提升到自己的合成層(隱式合成)。
  3. 提升到合成層總會造成一次重繪:瀏覽器必須為元素創(chuàng)建一個新的紋理,然后從前一個層中移除掉。
  4. 新層圖像必須發(fā)送到GPU中,用來合成用戶最終看到圖像。依層的數(shù)量、紋理尺寸和內(nèi)容復(fù)雜度的不同,重繪和數(shù)據(jù)傳輸可能花許多時間。這就是許多動畫在開始和結(jié)束時出現(xiàn)元素閃爍的原因。
  5. 動畫結(jié)束一剎那,我們從元素A上移除了合成的原因。此時,瀏覽器發(fā)現(xiàn)不需要浪費(fèi)資源來進(jìn)行合成,因此很快回到最優(yōu)策略:將頁面的整個內(nèi)容繪制在一個背景層當(dāng)中,這意味著必須把AB重繪回背景層當(dāng)中(另一次重繪),之后把更新的紋理發(fā)送給GPU。如上步驟,可能造成閃爍。

為了免受隱式合成問題的困擾,減少視覺假象,有如下建議:

  • 給予動畫元素盡可能高的z-index。理想情況下,這些元素應(yīng)該是body元素的直接子元素。當(dāng)然,動畫元素在DOM樹中嵌入很深、并且依賴常規(guī)流時,這是不大可能的。在此種情況下,你可以克隆該元素,將其放置到body中僅作動畫之用。
  • 你可以利用will-changeCSS屬性給瀏覽器一個提示,表明你要使用合成。將這個元素設(shè)置在元素上,瀏覽器會(并不總是)將其提前提升到一個合成層中,因此動畫能夠流暢地啟動和停止。但別濫用這個屬性,否則最終會導(dǎo)致內(nèi)存的急劇消耗!

僅將tranformopacity屬性動畫化

tranformopacity屬性能夠確保既不影響也不會被常規(guī)流或者DOM環(huán)境影響(也就是說,它們不會造成回流或者重繪,因此動畫完全交由GPU渲染)?;旧希@意味著你可以高效地實(shí)現(xiàn)移動、縮放、旋轉(zhuǎn)、透明變換動畫,并且只有仿射變換。有時,你可以用這些屬性模擬其它動畫類型。

舉個非常常見的例子:背景色變換?;痉椒ㄊ翘砑右粋€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>

在這個例子中,動畫完全運(yùn)行在CPU中,動畫的每個階段都會重繪。但我們可以讓動畫運(yùn)行在GPU上。我們可以在上面添加一層,將其不透明度動畫化,而不是background-color屬性:

<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>

這個動畫會更快、更流暢。但記住,可能會引起隱式合成和額外的內(nèi)存消耗。然而此種情況下,可以極大減少內(nèi)存消耗。

減少合成層的大小

看下面兩張圖,看到區(qū)別了嗎?
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/layer-size.html" height="130" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>
這兩個合成層從視覺上來看是一樣的,但第一個有40,000字節(jié)(30KB),第二個僅僅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>

差別在于物理尺寸,#a為100×100像素(100×100×4=40000bytes),而#b僅為10×10像素(10×10×4=400bytes),但通過transform: scale(10)縮放到100×100像素。因為#b是一個復(fù)合層,由于will-change屬性,transform在最終的圖像繪制過程中,將完全在GPU中進(jìn)行。

手法很簡單:通過widthheight屬性減少合成層的物理大小,之后通過transform: scale(…)放大紋理。當(dāng)然,這種把戲只能減少非常簡單的、純色層的內(nèi)存消耗。但是,舉個例子,如果你想為一個大的照片創(chuàng)建動畫,可以減少5%到10%的大小,之后放大;用戶可能看不出任何差別,而你可以節(jié)省好幾兆寶貴的內(nèi)存。

盡可能地使用CSS 變換和動畫

我們知道,通過CSS transform和animation的transformopacity動畫會自動創(chuàng)建合成層,并且運(yùn)行在GPU上。我們也可以通過JavaScript實(shí)現(xiàn)動畫,但為了元素獲取自己的合成層,必選先添加transform: translateZ(0)will-change: transform, opacity
JavaScript動畫的每一步是在requestAnimationFrame回調(diào)函數(shù)中手動計算的。通過Element.animate()實(shí)現(xiàn)的動畫是聲明式CSS動畫的變體。

一方面,通過CSS transition和animation創(chuàng)建簡單可復(fù)用的動畫非常容易;另一方面,創(chuàng)建包含漂亮軌跡的復(fù)雜動畫時,JavaScript動畫又比CSS動畫容易實(shí)現(xiàn)。另外,JavaScript是和用戶輸入交互的唯一方式。

哪一個更好?我們可以只用一個通用的JavaScript動畫庫來實(shí)現(xiàn)所有動畫嗎?

基于CSS的動畫有個很重要的特性:完全在GPU上運(yùn)行。因為你聲明了動畫如何開始和結(jié)束,瀏覽器可以趕在動畫開始之前,準(zhǔn)備好所需要的所有指令,之后發(fā)送給GPU。在必須使用JavaScript的情況下,瀏覽器所知的只有當(dāng)前幀的狀態(tài)。對一個流暢動畫而言,我們必須以每秒60次的速度在瀏覽器主線程中計算好新幀,然后發(fā)送給GPU。這些計算和數(shù)據(jù)發(fā)送不僅比CSS動畫慢,同時也依賴于主線程的工作負(fù)載:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/js-vs-css.html" height="180" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

在上面的例子當(dāng)中,當(dāng)主線程被繁重的JavaScript任務(wù)阻塞的時候,你會看到發(fā)生了什么。CSS動畫不受影響,因為新幀是在獨(dú)立的線程上計算的,而JavaScript動畫必須等到繁重的計算完成,之后才計算新幀。

因此,試著盡可能使用基于CSS的動畫,尤其是加載和進(jìn)度指示條。不僅更快,而且還不會被大量的JavaScript計算阻塞。

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

本篇文章是我在為 Chaos Fighters開發(fā)頁面時的研究和實(shí)驗結(jié)果。這是個響應(yīng)式的手機(jī)游戲促銷頁面,有大量的動畫。當(dāng)開始開發(fā)的時候,我唯一所知的就是如何實(shí)現(xiàn)基于GPU的動畫,但我并不知其工作原理。因此,在最初的里程碑頁,就造成了iPhone5——當(dāng)時最新的Apple手機(jī)——在頁面加載完幾秒鐘后崩潰了?,F(xiàn)在,這個頁面運(yùn)行良好,即使是在性能稍弱的設(shè)備上。

按照我的觀點(diǎn),讓我們考慮下這個網(wǎng)站中最有趣的優(yōu)化部分。

頁面的最頂端是游戲的介紹,有個像太陽光線東西在背景上旋轉(zhuǎn)。這是個無線循環(huán)、非交互的旋轉(zhuǎn)盤——正適合用簡單的CSS動畫實(shí)現(xiàn)。首先想到的方案(錯誤嘗試)是保存太陽光線的圖片,將它放在img中,之后使用無限CSS動畫:
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/gwBjqG/?height=402&theme-id=light&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

似乎如預(yù)期的那樣萬事大吉。但是太陽的圖片相當(dāng)大。移動用戶可能不開心了。

再仔細(xì)看下圖片。只是從圖片中心發(fā)出來幾道光線而已。光線是一樣的,因此我們可以保存單個光線,復(fù)用它來實(shí)現(xiàn)最終的圖片。最后,我們僅用了一個單光線的圖片,相比剛開始的圖片,大小少了一個數(shù)量級。

對于這種優(yōu)化,我們的標(biāo)記語言就必須復(fù)雜一點(diǎn)了:.sun是光線圖片元素的容器。每條光線在特定的角度旋轉(zhuǎn)。
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/qaJraq/?height=402&theme-id=light&default-tab=css&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

視覺效果是一樣的,但是網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量會少得多。另外,合成層的大小保持不變:500 × 500 × 4 ≈ 977 KB。

為了保證簡單,例子中太陽光線的大小是相當(dāng)小的,只有500 × 500像素。在實(shí)際網(wǎng)站中,服務(wù)于不同尺寸的設(shè)備(手機(jī)、平板和桌面電腦)和不同分辨率,最終圖片的大小大約000 × 3000 × 4 = 36 MB!而這僅僅是頁面中的一個動畫元素。

在“Layers”面板中再看下頁面的元素。通過旋轉(zhuǎn)整個太陽容器,使得動畫實(shí)現(xiàn)更容易。因此,這個容器被提升到一個合成層,被繪制進(jìn)一個大的紋理圖像中,之后發(fā)送給GPU。但是由于我們的簡化,現(xiàn)在紋理中包含無用的數(shù)據(jù):光線之間的間隔。

此外,無用的數(shù)據(jù)比有用的數(shù)據(jù)大很多!這不是利用有限內(nèi)存資源的最佳方式。

解決辦法和我們優(yōu)化網(wǎng)絡(luò)傳輸時一樣:只發(fā)送有用的數(shù)據(jù)(即光線)給GPU。我們可以計算下節(jié)約多少內(nèi)存:

  • 整個太陽容器:500 × 500 × 4 ≈ 977 KB
  • 12個太陽光線:250 × 40 × 4 × 12 ≈ 469 KB

內(nèi)存消耗可以減少一倍,為實(shí)現(xiàn)這一方案,我們必須為每個光線單獨(dú)實(shí)現(xiàn)動畫,而不是整個容器。因此,只有光線圖像會被發(fā)送到GPU當(dāng)中;它們之間的間隔不會占用任何資源。

為了實(shí)現(xiàn)獨(dú)立的光線動畫,標(biāo)記語言已經(jīng)有點(diǎn)復(fù)雜了,此處的CSS更是一個障礙。我們已經(jīng)為光線的初始旋轉(zhuǎn)使用了transform,必須從同樣的角度啟動動畫,然后旋轉(zhuǎn)360度?;旧希覀兊脼槊總€光線分別實(shí)現(xiàn)一個@keyframes,這是個不小的網(wǎng)絡(luò)傳輸。

光線的初始放置和微調(diào)動畫,光線數(shù)量等,寫個簡短的JavaScript來處理這些問題會更容易。

<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/bwmxoz/?height=402&theme-id=light&default-tab=js&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

新的動畫看起來和前一個一樣,但是內(nèi)存消耗只有一半。

還沒結(jié)束。從布局合成的角度來說,這個太陽動畫不是主元素,而是一個背景元素。并且光線沒有鮮明的對比元素。這意味著我們可以發(fā)送一個低分辨率的光線紋理給GPU,之后放大它,這可以節(jié)省一部分內(nèi)存。
我們試著減少10%的紋理大小。光線的物理尺寸為50 × 0.9 × 40 × 0.9 = 225 × 36 像素。為了使它看起來和250 × 20一樣,我們需要放大250 ÷ 225 ≈ 1.111倍。

我們會在代碼中加一行:給.sun-ray加上background-size: cover——這樣背景圖就會自動調(diào)整到元素的大小,并且為光線的動畫添加transform: scale(1.111)。
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/YGJOva/?height=402&theme-id=light&default-tab=js&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

注意,我們只改變了元素的大??;PNG圖片的大小仍然一樣。由DOM元素創(chuàng)建的矩形被渲染成紋理供GPU使用,而不是PNG圖片。

在GPU中,太陽光線的新合成大小現(xiàn)在為225 × 36 × 4 × 12 ≈ 380 KB(原來是469KB)。我們已經(jīng)減少了19%的內(nèi)存消耗,并且實(shí)現(xiàn)了非常靈活的代碼,可以通過縮放來實(shí)現(xiàn)最優(yōu)的質(zhì)量內(nèi)存比。因此,通過提高動畫(起先看起來很簡單)的復(fù)雜度,我們減少了內(nèi)存使用量977 ÷ 380 ≈ 2.5 倍!

我想你已經(jīng)發(fā)現(xiàn)了這個方法的缺陷:動畫現(xiàn)在工作在CPU上,可能被大量的JavaScript計算阻塞。如果你想更熟悉優(yōu)化GPU動畫,我留個小小的家庭作業(yè)。ForkCodepen of the sun rays,然后將太陽光線完全轉(zhuǎn)移到GPU上運(yùn)行,然而還要和初始的例子一樣節(jié)省內(nèi)存和靈活。將你的例子提交到注釋中,我會回復(fù)你的。

獲得的教訓(xùn)

優(yōu)化Chaos Fighters頁面的研究使我完全重新思考開發(fā)現(xiàn)代web頁面的過程。以下是我的主要原則:

  • 一定要和客戶端和設(shè)計者溝通網(wǎng)站上所有的動畫和效果。這可能極大地影響頁面的標(biāo)記語言,并且也有利于更好地合成。
  • 從一開始就要注意合成層的大小和數(shù)量——尤其是隱式合成層。瀏覽器開發(fā)者工具中的“Layers”面板是你最好的朋友。
  • 現(xiàn)代瀏覽器大量運(yùn)用合成,不僅僅是動畫,還有優(yōu)化頁面元素的繪制。比如,position: fixediframe、video元素也使用合成。
  • 合成層的大小可能比數(shù)量更重要。在某些情況下,瀏覽器會試圖減少合成層的數(shù)量(參見“GPU Accelerated Compositing in Chrome”中的“Layer Squashing”一節(jié));這會阻止所謂的“層爆炸”和減少內(nèi)存消耗,尤其是當(dāng)層有大量的交集時。但有時,這種優(yōu)化有副作用,比如當(dāng)一個很大的紋理消耗的內(nèi)存比多個小層多時。為了避免這種優(yōu)化,我給每個元素加了個小的、唯一的translateZ()值,比如translateZ(0.0001px),translateZ(0.0002px)等。瀏覽器會認(rèn)為處在3D空間的不同層,從而跳過優(yōu)化。
  • 為了從視覺上提高動畫的性能或者避免視覺假象,你不能只是簡單地給任何元素添加transform: translateZ(0)或者will-change: transform。GPU合成有許多缺點(diǎn)和權(quán)衡需要考慮。使用不當(dāng)時,可能會降低整體的性能,甚至導(dǎo)致瀏覽器崩潰。

請允許我再提醒下免責(zé)聲明:關(guān)于GPU合成,沒有任何官方規(guī)范,每個瀏覽器廠商解決同一個問題的方案不盡相同。本篇文章中的某些部分幾個月后可能就過時了。比如,Google Chrome 開發(fā)者正在想方法減少CPU和GPU之間數(shù)據(jù)傳輸?shù)拈_銷,包括使用特殊的共享內(nèi)存,這樣就沒有開銷了。另外,Safari已經(jīng)能夠?qū)⒑唵卧氐睦L制(比如具有background-color的空DOM元素)代理到GPU,而不是在CPU上為其創(chuàng)建圖像。

無論如何,我希望本篇文章已經(jīng)幫助你更好地理解瀏覽器采用GPU渲染的原理,從而幫你創(chuàng)建在各種設(shè)備上都能快速運(yùn)行的令人難忘的網(wǎng)站。

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

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

  • 譯者序:原文GPU Animation: Doing It Right,發(fā)表于2016年12月6日,本文是對該篇的...
    smilewalker閱讀 1,717評論 0 8
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,085評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,812評論 25 709
  • 我的茉莉清茶
    晴天兔子閱讀 234評論 0 0
  • 有一種神奇的生物叫做“同學(xué)”,只要你們在同一個教室上過一節(jié)課,他就會是你的同學(xué)。 他會出現(xiàn)在你的QQ里,微信里,微...
    zzz宇閱讀 649評論 0 0

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