最近看到一篇關(guān)于GPU動(dòng)畫的神文絮宁,原文地址:https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/样悟。
特此翻譯出來,供自己以及他人學(xué)習(xí)和查看馒胆。轉(zhuǎn)載請(qǐng)標(biāo)明出處稍坯,謝謝腰鬼。
另外那婉,由于簡(jiǎn)書和GitHub的markdown均不支持內(nèi)嵌iframe。而文中的大多數(shù)效果圖是嵌在iframe中的,可以移步github浑侥,下載后在支持內(nèi)嵌iframe的markdown環(huán)境下查看姊舵。
如今,絕大多數(shù)人知道現(xiàn)代瀏覽器采用GPU來渲染網(wǎng)頁的部分內(nèi)容寓落,尤其是動(dòng)畫部分括丁。比如,采用transform
屬性的CSS動(dòng)畫看起來比使用left
和top
屬性的動(dòng)畫更流暢零如。但如果你要問:“我如何利用GPU實(shí)現(xiàn)流暢的動(dòng)畫躏将?”絕大多數(shù)情況下,你會(huì)聽到如下回答:“使用transform: translateZ(0)
或者will-change: transform
考蕾』霰铮”
在開始GPU動(dòng)畫-或者合成(compositing,瀏覽器廠商喜歡這么稱呼)之前肖卧,從某種意義上來說蚯窥,這些個(gè)屬性就有點(diǎn)像IE6中使用的zoom:1
一樣。(譯者注:有點(diǎn)拗口塞帐,意思應(yīng)該是許多人只知道這樣設(shè)置就行了拦赠,但是并不知道具體的原因)
但有時(shí)候,簡(jiǎn)單demo中絲滑流暢的動(dòng)畫葵姥,在實(shí)際網(wǎng)站中運(yùn)行非常慢荷鼠,造成視覺假象,甚至讓瀏覽器崩潰榔幸。為什么會(huì)這樣允乐?如何修復(fù)?讓我們來了解一下削咆。
免責(zé)聲明
在深入研究GPU合成之前牍疏,我想告訴你們一件十分重要的事:這是一個(gè)大大的hack。至少到目前為止拨齐,合成的工作原理鳞陨,如何明確地將元素置于合成層,或者合成本身瞻惋,關(guān)于這些問題厦滤,你在W3C規(guī)范上找不到任何答案。它只是瀏覽器執(zhí)行特定任務(wù)時(shí)的一種優(yōu)化操作歼狼,每個(gè)瀏覽器廠商有自己的實(shí)現(xiàn)方式馁害。
本篇文章中你學(xué)到的所有東西,并不是合成原理的官方解釋蹂匹,而是我實(shí)驗(yàn)的結(jié)果,加上一點(diǎn)對(duì)瀏覽器子系統(tǒng)差異的常識(shí)和理解凹蜈。有些東西可能是錯(cuò)的限寞,有些可能隨著時(shí)間而變化——提醒過你了忍啸。
合成如何工作
在開始GPU動(dòng)畫頁面之前,我們得知道瀏覽器是如何工作的履植,不要簡(jiǎn)單地聽從網(wǎng)上的或者本篇文章中的一些隨意的建議计雌。
假設(shè)我們有一個(gè)頁面,其中有A
和B
兩個(gè)元素玫霎,每一個(gè)都設(shè)置了position: absolute
和不同的z-index
值凿滤。瀏覽器會(huì)用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動(dòng)畫,讓A
元素動(dòng)起來:
<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>
此種情況下鼻种,對(duì)于每個(gè)動(dòng)畫幀反番,瀏覽器都必須重新計(jì)算元素的位置(即reflow),渲染頁面新狀態(tài)的圖像(即repaint)叉钥,之后再發(fā)送給GPU顯示到屏幕上罢缸。我們知道,重繪是非常消耗性能的投队,但是每個(gè)現(xiàn)代瀏覽器都足夠智能枫疆,只重繪頁面中變化的部分,而不是整個(gè)頁面敷鸦。盡管絕大多數(shù)情況下息楔,瀏覽器可以很快地重繪,但我們的動(dòng)畫仍然不是太流暢轧膘。
在動(dòng)畫的每個(gè)階段回流钞螟、重繪整個(gè)頁面(即便是增量繪制),聽起來就很慢谎碍,尤其是又大又復(fù)雜的布局鳞滨。僅繪制兩個(gè)獨(dú)立的圖像可能更高效——一個(gè)為A元素,另一個(gè)為A元素以外的整個(gè)頁面——之后簡(jiǎn)單地偏移兩個(gè)圖片的相對(duì)位置蟆淀。也就是說拯啦,合成緩存元素的圖像可能更快。這就是GPU的優(yōu)勢(shì)所在:它能夠以亞像素精度快速合成圖像熔任,使得動(dòng)畫如絲般順滑褒链。
為了優(yōu)化合成,瀏覽器必須確保添加動(dòng)畫的CSS屬性:
- 不會(huì)影響文檔流疑苔,
- 不依賴文檔流甫匹,
- 不會(huì)造成重繪。
有人可能會(huì)以為,top
和left
屬性兵迅,輔之以position
為absolute
或fixed
抢韭,不依賴元素的環(huán)境,但其實(shí)并不是這樣恍箭。例如刻恭,left
屬性可能是個(gè)百分比值,其依賴于.offsetParent
的尺寸扯夭;另外鳍贾,em
,vh
和其它單位依賴于它們的環(huán)境交洗。相反骑科,transform
和opacity
是僅有的滿足上述條件的CSS屬性。
讓我們用transform
而不是left
來實(shí)現(xiàn)動(dòng)畫:
<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>
此處藕筋,我們以聲明的方式描述動(dòng)畫:起始位置纵散,結(jié)束位置,持續(xù)時(shí)間等隐圾。這等于提前告訴瀏覽器哪些CSS屬性會(huì)更新伍掀。因?yàn)闉g覽器發(fā)現(xiàn)沒有屬性會(huì)造成回流或者重繪,它就會(huì)采用合成優(yōu)化:畫兩幅圖像作為合成層暇藏,之后發(fā)送到GPU蜜笤。
這種優(yōu)化的優(yōu)點(diǎn)是什么呢?
- 我們獲得了一個(gè)亞像素精度的盐碱、如絲般順滑的動(dòng)畫把兔,運(yùn)行在專門為圖形任務(wù)優(yōu)化的單元上。并且運(yùn)行得非澄屯纾快县好。
- 動(dòng)畫再也不受限于CPU。即使運(yùn)行繁重的JavaScript任務(wù)暖混,動(dòng)畫依然很快缕贡。
一切聽起來似乎簡(jiǎn)單明了,不是嗎拣播?我們會(huì)遇到哪些問題晾咪?讓我們看看這種優(yōu)化的工作原理。
GPU是一個(gè)獨(dú)立的計(jì)算機(jī)贮配,這可能讓你覺得吃驚谍倦。確實(shí)如此:每個(gè)現(xiàn)代設(shè)備必不可缺的部分是一個(gè)獨(dú)立的單元,它有自己的處理器和內(nèi)存泪勒、數(shù)據(jù)處理模塊昼蛀。如同其它應(yīng)用和游戲一樣宴猾,瀏覽器必須與GPU進(jìn)行通信,好像和外設(shè)一樣曹洽。
為了更好地理解其工作原理鳍置,想像下Ajax。假設(shè)你想用填寫的表單數(shù)據(jù)注冊(cè)網(wǎng)站用戶送淆。你不能簡(jiǎn)單地告訴遠(yuǎn)端的服務(wù)器,“嗨怕轿,把這些表單數(shù)據(jù)和JavaScript變量保存到數(shù)據(jù)庫中偷崩。”遠(yuǎn)端數(shù)據(jù)庫無法訪問用戶瀏覽器的內(nèi)存撞羽。反而阐斜,你必須把頁面中的數(shù)據(jù)以易解析的格式(比如JSON),收集在一個(gè)payload中诀紊,然后發(fā)給遠(yuǎn)端服務(wù)器谒出。
合成的過程也差不多。GPU就像個(gè)遠(yuǎn)端的服務(wù)器邻奠,瀏覽器必須先創(chuàng)建一個(gè)payload笤喳,之后再發(fā)送給GPU。顯然碌宴,GPU不是遠(yuǎn)離CPU千里之外杀狡;它就在那。在很多情況下贰镣,對(duì)遠(yuǎn)端服務(wù)器的請(qǐng)求和響應(yīng)間隔時(shí)間在2S內(nèi)是可以接受的呜象。而對(duì)于GPU,3到5毫秒的延遲卻能導(dǎo)致動(dòng)畫卡頓碑隆。
GPU payload長(zhǎng)什么樣恭陡?一般由層圖像組成,還有一些附加的說明上煤,比如層的尺寸休玩,偏移,動(dòng)畫參數(shù)等楼入。以下是GPU的payload生成和傳輸?shù)拇蟾胚^程:
- 將每個(gè)合成層繪制為獨(dú)立的圖像
- 準(zhǔn)備層數(shù)據(jù)(尺寸哥捕,偏移,不透明度等)
- 為動(dòng)畫準(zhǔn)備著色器(如果可用的話)
- 發(fā)送數(shù)據(jù)給GPU
如你所見嘉熊,每次給元素添加神奇的transform: translateZ(0)
或者will-change: transform
屬性時(shí)遥赚,都開啟了同樣的過程。然而重繪是非常耗性能的阐肤,此時(shí)會(huì)變得更慢凫佛。多數(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
做成動(dòng)畫,它處在頁面所有元素之上毫炉。這會(huì)生成兩個(gè)合成層:A
元素一個(gè)瓮栗,B
元素和頁面背景一個(gè)。
現(xiàn)在瞄勾,我們讓B
元素動(dòng)起來:
<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>
我們遇到了一個(gè)邏輯問題费奸。元素B
應(yīng)該在一個(gè)獨(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
的上面糙麦。那么辛孵,瀏覽器會(huì)怎么做呢?
猜對(duì)了喳资!瀏覽器會(huì)強(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>
這稱為隱式合成:按照棧順序,一個(gè)或多個(gè)非合成元素出現(xiàn)在合成元素上面時(shí)仆邓,會(huì)被提升到合成層——即被繪制成獨(dú)立的圖像發(fā)送到GPU中鲜滩。
我們遇到隱式合成的情況比你想象的要頻繁的多。瀏覽器會(huì)因很多原因?qū)⒁粋€(gè)元素提升為合成層节值,比如:
- 3D 變換:
translate3d
徙硅,translateZ
等等; -
<video>
搞疗、<canvas>
和<iframe>
元素嗓蘑; - 通過
Element.animate()
實(shí)現(xiàn)的transform
和opacity
動(dòng)畫; - 通過CSS transition animation實(shí)現(xiàn)的
transform
和opacity
動(dòng)畫匿乃; -
position: fixed
桩皿; -
will-change
; -
filter
幢炸;
更多情況請(qǐng)參考Chromium項(xiàng)目的“CompositingReasons.h”文件泄隔。
似乎GPU動(dòng)畫的主要問題是意想不到的大量重繪。但并不是宛徊。最大的問題是佛嬉。逻澳。。
內(nèi)存消耗
再一次溫馨提示:GPU是獨(dú)立的計(jì)算機(jī):它不僅需要發(fā)送渲染好的圖片給GPU暖呕,而且需要對(duì)其進(jìn)行存儲(chǔ)斜做,以便后續(xù)動(dòng)畫復(fù)用。
一個(gè)合成層需要消耗多少內(nèi)存湾揽?讓我們看個(gè)簡(jiǎn)單點(diǎn)的例子瓤逼。猜猜存儲(chǔ)一個(gè)320×240像素,填滿#FF0000
顏色的長(zhǎng)方形需要多少內(nèi)存钝腺。
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/rect.html" height="270" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>
一個(gè)標(biāo)準(zhǔn)的web開發(fā)者這樣想:“嗯抛姑,這是個(gè)純色的圖像,我會(huì)將其保存為PNG然后查看其大小艳狐。應(yīng)該小于1KB”。沒錯(cuò)皿桑,這個(gè)PNG圖片大概104字節(jié)毫目。
問題是,PNG以及JPEG诲侮,GIF等镀虐,用來存儲(chǔ)和傳輸圖像數(shù)據(jù)。為了將這樣的圖像繪制到屏幕上沟绪,計(jì)算機(jī)必須解壓圖像數(shù)據(jù)刮便,然后表示成像素?cái)?shù)組。因此绽慈,我們的樣圖會(huì)消耗320 × 240 × 3 = 230,400 bytes
的內(nèi)存恨旱。也就是,圖片寬度乘以高度獲得圖片的像素?cái)?shù)坝疼。之后再乘以3搜贤,因?yàn)槊總€(gè)像素由3個(gè)字節(jié)描述(RGB)。如果圖片包含透明通道钝凶,就得乘以4仪芒,因?yàn)楦郊拥囊粋€(gè)字節(jié)用來描述透明度(RGBa):320 × 240 × 4 = 307,200 bytes
。
瀏覽器總是按照RGBa圖像的形式繪制合成層耕陷。似乎沒有行之有效的方法來確定圖片是否包含了透明通道掂名。
再看一個(gè)更常見的例子:一個(gè)有10張圖的旋轉(zhuǎn)盤,每張圖800×600像素哟沫。我們希望用戶交互饺蔑,比如拖拽時(shí),圖片之間能夠平滑過渡南用,因此膀钠,我們?yōu)槊糠鶊D添加will-change: transform
掏湾。這會(huì)提前將圖片提升到合成層,因此肿嘲,用戶一開始交互時(shí)融击,過渡就會(huì)開始。現(xiàn)在計(jì)算下僅僅展示這一旋轉(zhuǎn)盤需要多少額外內(nèi)存:800 × 600 × 4 × 10 ≈ 19 MB雳窟。
僅僅一個(gè)控制點(diǎn)就需要額外19MB內(nèi)存尊浪!如果你是一個(gè)單頁應(yīng)用的WEB開發(fā)者,頁面中有多個(gè)動(dòng)畫控制點(diǎn)封救,視差效果拇涤,高分辨率圖像和其它視覺增強(qiáng)效果,那么每個(gè)頁面多增加100到200MB僅僅是個(gè)開始誉结。再考慮上隱式合成的話(承認(rèn)吧——你之前根本沒想過這個(gè))鹅士,最終頁面會(huì)耗盡設(shè)備的內(nèi)存。
此外惩坑,多數(shù)情況下掉盅,這些內(nèi)存會(huì)被浪費(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>
對(duì)于桌面客戶端來說以舒,這可能不是個(gè)問題趾痘,但會(huì)深深刺痛移動(dòng)用戶的心。首先蔓钟,絕大多數(shù)現(xiàn)代設(shè)備擁有高分辨率的屏幕:這就將合成層圖片的體量乘以4到9永票。其次,移動(dòng)設(shè)備不像桌面設(shè)備那樣有那么大的內(nèi)存滥沫。比如侣集,不是太舊的iPhone6僅搭載1GB共享內(nèi)存(即,內(nèi)存同時(shí)用于RAM和VRAM)佣谐《抢簦考慮到至少三分之一的內(nèi)存用于操作系統(tǒng)和后臺(tái)進(jìn)程,另外的三分之一用于瀏覽器和當(dāng)前頁面(最好的情況是高度優(yōu)化的頁面狭魂,沒有太多的framework)罚攀,我們至多剩下200到300MB供GPU渲染。并且iPhone6是個(gè)相當(dāng)昂貴的高端設(shè)備雌澄,更多平價(jià)的手機(jī)所搭載的內(nèi)存更少斋泄。
你也許會(huì)問:“有可能在GPU上存儲(chǔ)PNG圖片來減少內(nèi)存占用嗎?”技術(shù)上是可行的镐牺。唯一的問題是GPU在屏幕上是逐像素繪制的炫掐,這意味著它必須一次次地解碼整個(gè)PNG圖片來獲取每個(gè)像素?cái)?shù)據(jù)。我懷疑這種情況下的動(dòng)畫比每秒一幀快點(diǎn)睬涧。
GPU特定的圖像壓縮格式確實(shí)存在募胃,但毫無意義旗唁。從壓縮比來看,根本比不上PNG或者JPEG痹束,并且使用上也缺乏硬件支持检疫。
優(yōu)缺點(diǎn)
既然學(xué)了些GPU動(dòng)畫的基本原理,讓我們總結(jié)下它的優(yōu)缺點(diǎn):
優(yōu)點(diǎn)
- 動(dòng)畫既快又流暢祷嘶,達(dá)到每秒60幀屎媳。
- 精心制作的動(dòng)畫在獨(dú)立的線程中運(yùn)行,不會(huì)被繁重的JavaScript計(jì)算阻塞
- 3D變換很“廉價(jià)”
缺點(diǎn)
- 需要附加的重繪來將元素提升到合成層论巍。有時(shí)這個(gè)過程很慢(比如烛谊,進(jìn)行全層重繪,而不是增量重繪)嘉汰。
- 繪制的層必須傳到GPU中丹禀。依據(jù)層的大小和數(shù)量,傳輸可能很慢鞋怀。這可能導(dǎo)致中低端設(shè)備上元素閃爍湃崩。
- 每個(gè)合成層消耗額外的內(nèi)存。在移動(dòng)設(shè)備上接箫,內(nèi)存是寶貴的資源。內(nèi)存超標(biāo)使用會(huì)使瀏覽器崩潰朵诫。
- 如果你不考慮隱式合成辛友,重繪緩慢、額外內(nèi)存使用和瀏覽器崩潰的可能性會(huì)很高。
- 我們會(huì)看到視覺假象,比如Safari上文本渲染余舶,在某些情況下适肠,頁面內(nèi)容會(huì)消失或者混亂。
如你所見砸西,盡管有些獨(dú)特的優(yōu)勢(shì),GPU動(dòng)畫仍然有些令人討厭的問題。最重要的是重繪和大量的內(nèi)存消耗掖看;因此,以下所有的優(yōu)化策略都是處理這些問題的面哥。
瀏覽器設(shè)置
在開始優(yōu)化之前哎壳,我們得學(xué)習(xí)一些工具,來幫助我們檢查頁面的合成層尚卫,并且提供合理的優(yōu)化反饋归榕。
Safari
Safari的web 監(jiān)視器有個(gè)很棒的“Layers”邊條,它顯示所有的層以及內(nèi)存消耗吱涉,以及合成的原因刹泄。來看看這個(gè)邊條:
- 在Safari中外里,按
? + ? + I
打開web監(jiān)視器。如果不起作用特石,打開“Preferences” → “Advanced”盅蝗,開啟“Show Develop Menu in menu bar”選項(xiàng),再試一次县匠。 - 當(dāng)web監(jiān)視器打開后风科,選擇“Elements”面板,選擇右邊條的“Layers”乞旦。
- 現(xiàn)在贼穆,當(dāng)你在主“Elements”上點(diǎn)擊一個(gè)DOM元素時(shí),你會(huì)看到一個(gè)關(guān)于選擇元素以及所有后代合成層的信息層(如果使用了合成的話)兰粉。
-
點(diǎn)擊一個(gè)后代層故痊,查看其合成原因。瀏覽器會(huì)告訴你為什么決定把這個(gè)元素遷移至它自己的合成層玖姑。
Chrome
Chrome的開發(fā)者工具欄有個(gè)類似的面板愕秫,但你必須首先激活它:
- 在Chrome中,訪問
chrome://flags/#enable-devtools-experiments
焰络,之后啟用“Developer Tools experiments”項(xiàng)戴甩。 - 用
? + ? + I
(Mac)或者Ctrl + Shift + I
(PC)打開工具欄,之后點(diǎn)擊右上角的如下圖標(biāo)闪彼,選擇“Setting”菜單項(xiàng):
- 回到“Experiments”面板甜孤,啟用“Layers”面板。
-
重新打開開發(fā)者工具欄∥吠螅現(xiàn)在缴川,你就能看到“Layers”面板了。
這個(gè)面板以樹的形式展示當(dāng)前頁面所有活動(dòng)的合成層描馅。當(dāng)選擇一個(gè)層的時(shí)候把夸,你會(huì)看到諸如尺寸,內(nèi)存消耗铭污,重繪次數(shù)和合成原因恋日。
優(yōu)化建議
現(xiàn)在我們已經(jīng)設(shè)置好環(huán)境,可以開始優(yōu)化合成層了况凉。我們已經(jīng)確定了合成的兩個(gè)主要問題:額外的重繪谚鄙,也會(huì)造成數(shù)據(jù)數(shù)據(jù)傳送到GPU,還有額外的內(nèi)存消耗刁绒。因此闷营,以下所有的優(yōu)化建議都是針對(duì)這兩個(gè)問題的。
避免隱式合成
這是最簡(jiǎn)單明了的建議,同樣也十分重要傻盟。提醒你一下速蕊,處在一個(gè)顯式合成層(比如position: fixed
,視頻娘赴,CSS動(dòng)畫等)之上的所有非合成DOM元素规哲,會(huì)被強(qiáng)制提升到自己的層,僅僅為了最后的GPU圖像合成诽表。在移動(dòng)設(shè)備上唉锌,可能會(huì)導(dǎo)致動(dòng)畫啟動(dòng)緩慢。
舉個(gè)簡(jiǎn)單的例子:
<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
元素是個(gè)需要用戶交互啟動(dòng)的動(dòng)畫竿奏。如果你在“Layers”面板中查看這個(gè)頁面袄简,你會(huì)發(fā)現(xiàn)沒有多余的層。但當(dāng)點(diǎn)擊“Play”按鈕后泛啸,你會(huì)看到更多的層绿语,這些層在動(dòng)畫結(jié)束后立即被移除。如果看下“Timeline”面板候址,會(huì)發(fā)現(xiàn)動(dòng)畫的開始和結(jié)束位置充斥大片區(qū)域的重繪:
![](https://www.smashingmagazine.com/wp-content/uploads/2016/11/chrome-timeline-large-opt.png)
以下是瀏覽器所做的吕粹,一步接一步:
- 頁面加載完成后,瀏覽器找不到任何合成的理由岗仑,因此選擇了最優(yōu)的策略:在單個(gè)背景層上繪制整個(gè)頁面內(nèi)容匹耕。
- 點(diǎn)擊“Play”按鈕,我們給元素
A
顯式增加了合成層——transfrom
屬性的一個(gè)變換荠雕。但是瀏覽器發(fā)現(xiàn)按照棧順序泌神,元素A
在元素B
下面,因此也將B
提升到自己的合成層(隱式合成)舞虱。 - 提升到合成層總會(huì)造成一次重繪:瀏覽器必須為元素創(chuàng)建一個(gè)新的紋理,然后從前一個(gè)層中移除掉母市。
- 新層圖像必須發(fā)送到GPU中矾兜,用來合成用戶最終看到圖像。依層的數(shù)量患久、紋理尺寸和內(nèi)容復(fù)雜度的不同椅寺,重繪和數(shù)據(jù)傳輸可能花許多時(shí)間。這就是許多動(dòng)畫在開始和結(jié)束時(shí)出現(xiàn)元素閃爍的原因蒋失。
- 動(dòng)畫結(jié)束一剎那返帕,我們從元素
A
上移除了合成的原因。此時(shí)篙挽,瀏覽器發(fā)現(xiàn)不需要浪費(fèi)資源來進(jìn)行合成荆萤,因此很快回到最優(yōu)策略:將頁面的整個(gè)內(nèi)容繪制在一個(gè)背景層當(dāng)中,這意味著必須把A
和B
重繪回背景層當(dāng)中(另一次重繪),之后把更新的紋理發(fā)送給GPU链韭。如上步驟偏竟,可能造成閃爍。
為了免受隱式合成問題的困擾敞峭,減少視覺假象踊谋,有如下建議:
- 給予動(dòng)畫元素盡可能高的
z-index
。理想情況下旋讹,這些元素應(yīng)該是body
元素的直接子元素殖蚕。當(dāng)然,動(dòng)畫元素在DOM樹中嵌入很深沉迹、并且依賴常規(guī)流時(shí)睦疫,這是不大可能的。在此種情況下胚股,你可以克隆該元素笼痛,將其放置到body中僅作動(dòng)畫之用。 - 你可以利用
will-change
CSS屬性給瀏覽器一個(gè)提示琅拌,表明你要使用合成缨伊。將這個(gè)元素設(shè)置在元素上,瀏覽器會(huì)(并不總是)將其提前提升到一個(gè)合成層中进宝,因此動(dòng)畫能夠流暢地啟動(dòng)和停止刻坊。但別濫用這個(gè)屬性,否則最終會(huì)導(dǎo)致內(nèi)存的急劇消耗党晋!
僅將tranform
和opacity
屬性動(dòng)畫化
tranform
和opacity
屬性能夠確保既不影響也不會(huì)被常規(guī)流或者DOM環(huán)境影響(也就是說谭胚,它們不會(huì)造成回流或者重繪,因此動(dòng)畫完全交由GPU渲染)未玻≡侄基本上,這意味著你可以高效地實(shí)現(xiàn)移動(dòng)扳剿、縮放旁趟、旋轉(zhuǎn)、透明變換動(dòng)畫庇绽,并且只有仿射變換锡搜。有時(shí),你可以用這些屬性模擬其它動(dòng)畫類型瞧掺。
舉個(gè)非常常見的例子:背景色變換耕餐。基本方法是添加一個(gè)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>
在這個(gè)例子中辟狈,動(dòng)畫完全運(yùn)行在CPU中肠缔,動(dòng)畫的每個(gè)階段都會(huì)重繪。但我們可以讓動(dòng)畫運(yùn)行在GPU上。我們可以在上面添加一層桩砰,將其不透明度動(dòng)畫化拓春,而不是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>
這個(gè)動(dòng)畫會(huì)更快、更流暢亚隅。但記住硼莽,可能會(huì)引起隱式合成和額外的內(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>
這兩個(gè)合成層從視覺上來看是一樣的匆光,但第一個(gè)有40,000字節(jié)(30KB)酿联,第二個(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>
差別在于物理尺寸贞让,#a
為100×100像素(100×100×4=40000bytes)周崭,而#b
僅為10×10像素(10×10×4=400bytes),但通過transform: scale(10)
縮放到100×100像素喳张。因?yàn)?code>#b是一個(gè)復(fù)合層续镇,由于will-change
屬性,transform
在最終的圖像繪制過程中销部,將完全在GPU中進(jìn)行摸航。
手法很簡(jiǎn)單:通過width
和height
屬性減少合成層的物理大小,之后通過transform: scale(…)
放大紋理舅桩。當(dāng)然酱虎,這種把戲只能減少非常簡(jiǎn)單的、純色層的內(nèi)存消耗擂涛。但是逢净,舉個(gè)例子,如果你想為一個(gè)大的照片創(chuàng)建動(dòng)畫歼指,可以減少5%到10%的大小,之后放大甥雕;用戶可能看不出任何差別踩身,而你可以節(jié)省好幾兆寶貴的內(nèi)存。
盡可能地使用CSS 變換和動(dòng)畫
我們知道社露,通過CSS transform和animation的transform
和opacity
動(dòng)畫會(huì)自動(dòng)創(chuàng)建合成層挟阻,并且運(yùn)行在GPU上。我們也可以通過JavaScript實(shí)現(xiàn)動(dòng)畫,但為了元素獲取自己的合成層附鸽,必選先添加transform: translateZ(0)
或will-change: transform, opacity
脱拼。
JavaScript動(dòng)畫的每一步是在requestAnimationFrame
回調(diào)函數(shù)中手動(dòng)計(jì)算的。通過Element.animate()
實(shí)現(xiàn)的動(dòng)畫是聲明式CSS動(dòng)畫的變體坷备。
一方面熄浓,通過CSS transition和animation創(chuàng)建簡(jiǎn)單可復(fù)用的動(dòng)畫非常容易;另一方面省撑,創(chuàng)建包含漂亮軌跡的復(fù)雜動(dòng)畫時(shí)赌蔑,JavaScript動(dòng)畫又比CSS動(dòng)畫容易實(shí)現(xiàn)。另外竟秫,JavaScript是和用戶輸入交互的唯一方式娃惯。
哪一個(gè)更好?我們可以只用一個(gè)通用的JavaScript動(dòng)畫庫來實(shí)現(xiàn)所有動(dòng)畫嗎肥败?
基于CSS的動(dòng)畫有個(gè)很重要的特性:完全在GPU上運(yùn)行趾浅。因?yàn)槟?strong>聲明了動(dòng)畫如何開始和結(jié)束,瀏覽器可以趕在動(dòng)畫開始之前馒稍,準(zhǔn)備好所需要的所有指令皿哨,之后發(fā)送給GPU。在必須使用JavaScript的情況下筷黔,瀏覽器所知的只有當(dāng)前幀的狀態(tài)往史。對(duì)一個(gè)流暢動(dòng)畫而言,我們必須以每秒60次的速度在瀏覽器主線程中計(jì)算好新幀佛舱,然后發(fā)送給GPU椎例。這些計(jì)算和數(shù)據(jù)發(fā)送不僅比CSS動(dòng)畫慢,同時(shí)也依賴于主線程的工作負(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ù)阻塞的時(shí)候订歪,你會(huì)看到發(fā)生了什么。CSS動(dòng)畫不受影響肆捕,因?yàn)樾聨窃讵?dú)立的線程上計(jì)算的刷晋,而JavaScript動(dòng)畫必須等到繁重的計(jì)算完成,之后才計(jì)算新幀慎陵。
因此眼虱,試著盡可能使用基于CSS的動(dòng)畫,尤其是加載和進(jìn)度指示條席纽。不僅更快捏悬,而且還不會(huì)被大量的JavaScript計(jì)算阻塞。
現(xiàn)實(shí)世界中優(yōu)化的例子
本篇文章是我在為 Chaos Fighters開發(fā)頁面時(shí)的研究和實(shí)驗(yàn)結(jié)果润梯。這是個(gè)響應(yīng)式的手機(jī)游戲促銷頁面过牙,有大量的動(dòng)畫甥厦。當(dāng)開始開發(fā)的時(shí)候,我唯一所知的就是如何實(shí)現(xiàn)基于GPU的動(dòng)畫寇钉,但我并不知其工作原理刀疙。因此,在最初的里程碑頁扫倡,就造成了iPhone5——當(dāng)時(shí)最新的Apple手機(jī)——在頁面加載完幾秒鐘后崩潰了∏恚現(xiàn)在,這個(gè)頁面運(yùn)行良好镊辕,即使是在性能稍弱的設(shè)備上油够。
按照我的觀點(diǎn),讓我們考慮下這個(gè)網(wǎng)站中最有趣的優(yōu)化部分征懈。
頁面的最頂端是游戲的介紹石咬,有個(gè)像太陽光線東西在背景上旋轉(zhuǎn)。這是個(gè)無線循環(huán)卖哎、非交互的旋轉(zhuǎn)盤——正適合用簡(jiǎn)單的CSS動(dòng)畫實(shí)現(xiàn)鬼悠。首先想到的方案(錯(cuò)誤嘗試)是保存太陽光線的圖片,將它放在img
中亏娜,之后使用無限CSS動(dòng)畫:
<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)大。移動(dòng)用戶可能不開心了维贺。
再仔細(xì)看下圖片它掂。只是從圖片中心發(fā)出來幾道光線而已。光線是一樣的溯泣,因此我們可以保存單個(gè)光線虐秋,復(fù)用它來實(shí)現(xiàn)最終的圖片。最后垃沦,我們僅用了一個(gè)單光線的圖片客给,相比剛開始的圖片,大小少了一個(gè)數(shù)量級(jí)肢簿。
對(duì)于這種優(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ù)量會(huì)少得多。另外收夸,合成層的大小保持不變:500 × 500 × 4 ≈ 977 KB坑匠。
為了保證簡(jiǎn)單,例子中太陽光線的大小是相當(dāng)小的咱圆,只有500 × 500像素笛辟。在實(shí)際網(wǎng)站中,服務(wù)于不同尺寸的設(shè)備(手機(jī)序苏、平板和桌面電腦)和不同分辨率手幢,最終圖片的大小大約000 × 3000 × 4 = 36 MB!而這僅僅是頁面中的一個(gè)動(dòng)畫元素忱详。
在“Layers”面板中再看下頁面的元素围来。通過旋轉(zhuǎn)整個(gè)太陽容器,使得動(dòng)畫實(shí)現(xiàn)更容易匈睁。因此监透,這個(gè)容器被提升到一個(gè)合成層,被繪制進(jìn)一個(gè)大的紋理圖像中航唆,之后發(fā)送給GPU胀蛮。但是由于我們的簡(jiǎn)化,現(xiàn)在紋理中包含無用的數(shù)據(jù):光線之間的間隔糯钙。
此外粪狼,無用的數(shù)據(jù)比有用的數(shù)據(jù)大很多!這不是利用有限內(nèi)存資源的最佳方式任岸。
解決辦法和我們優(yōu)化網(wǎng)絡(luò)傳輸時(shí)一樣:只發(fā)送有用的數(shù)據(jù)(即光線)給GPU再榄。我們可以計(jì)算下節(jié)約多少內(nèi)存:
- 整個(gè)太陽容器:500 × 500 × 4 ≈ 977 KB
- 12個(gè)太陽光線:250 × 40 × 4 × 12 ≈ 469 KB
內(nèi)存消耗可以減少一倍,為實(shí)現(xiàn)這一方案享潜,我們必須為每個(gè)光線單獨(dú)實(shí)現(xiàn)動(dòng)畫困鸥,而不是整個(gè)容器。因此剑按,只有光線圖像會(huì)被發(fā)送到GPU當(dāng)中疾就;它們之間的間隔不會(huì)占用任何資源。
為了實(shí)現(xiàn)獨(dú)立的光線動(dòng)畫吕座,標(biāo)記語言已經(jīng)有點(diǎn)復(fù)雜了虐译,此處的CSS更是一個(gè)障礙。我們已經(jīng)為光線的初始旋轉(zhuǎn)使用了transform
吴趴,必須從同樣的角度啟動(dòng)動(dòng)畫漆诽,然后旋轉(zhuǎn)360度÷嘀Γ基本上厢拭,我們得為每個(gè)光線分別實(shí)現(xiàn)一個(gè)@keyframes
,這是個(gè)不小的網(wǎng)絡(luò)傳輸撇叁。
光線的初始放置和微調(diào)動(dòng)畫供鸠,光線數(shù)量等,寫個(gè)簡(jiǎn)短的JavaScript來處理這些問題會(huì)更容易陨闹。
<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>
新的動(dòng)畫看起來和前一個(gè)一樣楞捂,但是內(nèi)存消耗只有一半薄坏。
還沒結(jié)束。從布局合成的角度來說寨闹,這個(gè)太陽動(dòng)畫不是主元素胶坠,而是一個(gè)背景元素。并且光線沒有鮮明的對(duì)比元素繁堡。這意味著我們可以發(fā)送一個(gè)低分辨率的光線紋理給GPU沈善,之后放大它,這可以節(jié)省一部分內(nèi)存椭蹄。
我們?cè)囍鴾p少10%的紋理大小闻牡。光線的物理尺寸為50 × 0.9 × 40 × 0.9 = 225 × 36 像素。為了使它看起來和250 × 20一樣绳矩,我們需要放大250 ÷ 225 ≈ 1.111倍罩润。
我們會(huì)在代碼中加一行:給.sun-ray
加上background-size: cover
——這樣背景圖就會(huì)自動(dòng)調(diào)整到元素的大小,并且為光線的動(dòng)畫添加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ì)量?jī)?nèi)存比。因此嗜桌,通過提高動(dòng)畫(起先看起來很簡(jiǎn)單)的復(fù)雜度奥溺,我們減少了內(nèi)存使用量977 ÷ 380 ≈ 2.5 倍!
我想你已經(jīng)發(fā)現(xiàn)了這個(gè)方法的缺陷:動(dòng)畫現(xiàn)在工作在CPU上骨宠,可能被大量的JavaScript計(jì)算阻塞浮定。如果你想更熟悉優(yōu)化GPU動(dòng)畫,我留個(gè)小小的家庭作業(yè)层亿。ForkCodepen of the sun rays桦卒,然后將太陽光線完全轉(zhuǎn)移到GPU上運(yùn)行,然而還要和初始的例子一樣節(jié)省內(nèi)存和靈活匿又。將你的例子提交到注釋中方灾,我會(huì)回復(fù)你的。
獲得的教訓(xùn)
優(yōu)化Chaos Fighters頁面的研究使我完全重新思考開發(fā)現(xiàn)代web頁面的過程碌更。以下是我的主要原則:
- 一定要和客戶端和設(shè)計(jì)者溝通網(wǎng)站上所有的動(dòng)畫和效果裕偿。這可能極大地影響頁面的標(biāo)記語言洞慎,并且也有利于更好地合成。
- 從一開始就要注意合成層的大小和數(shù)量——尤其是隱式合成層嘿棘。瀏覽器開發(fā)者工具中的“Layers”面板是你最好的朋友拢蛋。
- 現(xiàn)代瀏覽器大量運(yùn)用合成,不僅僅是動(dòng)畫蔫巩,還有優(yōu)化頁面元素的繪制。比如快压,
position: fixed
和iframe
圆仔、video
元素也使用合成。 - 合成層的大小可能比數(shù)量更重要蔫劣。在某些情況下坪郭,瀏覽器會(huì)試圖減少合成層的數(shù)量(參見“GPU Accelerated Compositing in Chrome”中的“Layer Squashing”一節(jié));這會(huì)阻止所謂的“層爆炸”和減少內(nèi)存消耗脉幢,尤其是當(dāng)層有大量的交集時(shí)歪沃。但有時(shí),這種優(yōu)化有副作用嫌松,比如當(dāng)一個(gè)很大的紋理消耗的內(nèi)存比多個(gè)小層多時(shí)沪曙。為了避免這種優(yōu)化,我給每個(gè)元素加了個(gè)小的萎羔、唯一的
translateZ()
值液走,比如translateZ(0.0001px)
,translateZ(0.0002px)
等贾陷。瀏覽器會(huì)認(rèn)為處在3D空間的不同層缘眶,從而跳過優(yōu)化。 - 為了從視覺上提高動(dòng)畫的性能或者避免視覺假象髓废,你不能只是簡(jiǎn)單地給任何元素添加
transform: translateZ(0)
或者will-change: transform
巷懈。GPU合成有許多缺點(diǎn)和權(quán)衡需要考慮。使用不當(dāng)時(shí)慌洪,可能會(huì)降低整體的性能顶燕,甚至導(dǎo)致瀏覽器崩潰。
請(qǐng)?jiān)试S我再提醒下免責(zé)聲明:關(guān)于GPU合成蒋譬,沒有任何官方規(guī)范割岛,每個(gè)瀏覽器廠商解決同一個(gè)問題的方案不盡相同。本篇文章中的某些部分幾個(gè)月后可能就過時(shí)了犯助。比如癣漆,Google Chrome 開發(fā)者正在想方法減少CPU和GPU之間數(shù)據(jù)傳輸?shù)拈_銷,包括使用特殊的共享內(nèi)存剂买,這樣就沒有開銷了惠爽。另外癌蓖,Safari已經(jīng)能夠?qū)⒑?jiǎn)單元素的繪制(比如具有background-color
的空DOM元素)代理到GPU,而不是在CPU上為其創(chuàng)建圖像婚肆。
無論如何租副,我希望本篇文章已經(jīng)幫助你更好地理解瀏覽器采用GPU渲染的原理,從而幫你創(chuàng)建在各種設(shè)備上都能快速運(yùn)行的令人難忘的網(wǎng)站较性。