譯者序:原文GPU Animation: Doing It Right,發表于2016年12月6日,本文是對該篇的中文翻譯,如有幫助,作為譯者,也深感欣慰。
附原文鏈接:https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/
目前,大部分人都知道現代瀏覽器是使用GPU來渲染web的部分頁面,尤其是帶有動畫的。舉個例子,一個使用transform
的css動畫看起來會比使用left
和top
屬性的更為流暢。但是如果你問,“我是如何從GPU獲得平滑的動畫?”多數情況下,你可能會聽到比如“使用 transform: translateZ(0)
或者 will-change: transform
。”的回答。
這些屬性好比如我們在IE6使用zoom:1
(如果你懂我的意思),用于準備GPU的動畫——或者合成(compositing),瀏覽器供應商喜歡這么稱它。
但有時,簡單演示中運行的很好很流暢的動畫,在真實網站卻很慢,引起視覺錯誤甚至導致瀏覽器崩潰。為什么會產生這種現象?我們如何修復它?接下來一起試著理解吧。
免責聲明
在我們深入GPU的合成前,我想告訴你一件重要的事:這是一個巨大的hack。你不會在W3C的規范里(至少目前來說)找到任何關于合成(compositing )如何工作的資料,如何顯式地在合成層上放置元素,甚至于合成本身。它只是瀏覽器用于執行確定任務的優化,并且每個瀏覽器供應商以自己的方式實現。
你在這篇文章學到的一切,不是官方說明文檔,而是我個人實驗的結果,夾雜著一點常識和不同瀏覽器子系統運行原理的知識。部分可能絕對是錯的,部分可能隨著時間而變化——這個要事先說明!
合成(Compositing )的工作原理
為了準備GPU動畫的頁面,我們需要理解瀏覽器的工作原理,而不僅僅是聽取來自網上或本文的隨意建議。
比如說一個頁面有 A
和 B
的元素,均為絕對定位position: absolute
,帶著不同的 z-index
。瀏覽器將會從CPU繪制,然后把生成的圖像發送給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>
現在用left
屬性和css的animation,來移動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>
在這種情況下,對于每個動畫幀,瀏覽器都會重新計算元素的幾何形狀(即回流reflow),渲染頁面新狀態的圖像 (即重繪repaint),然后再次將其發給GPU以顯示在屏幕。我們知道重繪是很耗性能成本的,每個現代瀏覽器都足夠快速的來重繪頁面改變的部分,而不是整個頁面。瀏覽器在多數情況下能都很快地重繪,但我們的動畫依舊不平滑。
在動畫的每一步(甚至遞增)進行回流和重繪整個頁面,聽起來真的很慢,特別是對于一個龐大復雜的布局。而繪制兩個獨立的圖像會更有效——一個是A
元素,一個是沒有A
元素的整個頁面——然后簡單的相對彼此偏移那些圖像。換句話來說,合成(composing)緩存的元素圖像會更快。這也是GPU閃光的地方:它能快速合成帶有亞像素精度的圖像,為動畫添加“性感“的平滑度。
為了優化合成,瀏覽器得確保css的動畫屬性:
- 不影響文檔流,
- 不依賴于文檔流,
- 不會造成重繪。
有人會認為帶有position: absolute
以及fixed
的top
和left
屬性,不依賴于其環境,但事實并非如此。比如說,值為百分比的left
屬性,會取決于.offsetParent
的大小;同樣,em
, vh
以及其他單位也會取決于自身環境。而transform
和opacity
是css唯一會滿足上述情況的屬性。
下面用transform
代替left
來動畫:
<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>
這里,我們聲明式地描述了動畫:它的開始位置,結束位置,持續時間等。它將提前告訴瀏覽器css的更新屬性。因為瀏覽器如果知道沒有任何屬性會導致回流或重繪,它可以應用合成優化:繪制合成層(compositing layers)的圖像并發給GPU。
這種優化的優點在哪?
- 得到一個帶有亞像素精密度的柔順平滑動畫,運行在特別為圖形任務的優化的單元上,并且非常快。
- 動畫不再綁定到CPU。即使運行一個強化的JavaScript任務,動畫依然會快速執行。
一切看起來如此的清楚和簡單,對吧,那會遇到什么問題呢?一起來看看這種優化方式是如何工作的。
它可能會讓你震驚,GPU竟是一個獨立的計算機。是的,每個現代設備的重要部分通常都是獨立單元,有自己的處理器,自己的內存和數據處理模塊。就像其他任何應用程序或者游戲一樣,瀏覽器需要用外部設備跟GPU通信。
為了更好的理解它是怎么工作的,想想AJAX吧。假使你要提交用戶輸入的數據,你不會告訴遠程服務器,“嗨,過來獲取這些輸入框的數據和JS變量,并保存到數據庫。”遠程服務器不能訪問用戶瀏覽器的內存。取而代之的是,你需要從頁面保存這些數據到可以輕松解析的簡單數據格式(如JSON)的有效內容中,并發送給遠程服務器。
合成也是如此。GPU就像遠程服務器,瀏覽器需要首先創建一個有效載荷,然后發送到設備。當然,GPU沒有距離CPU數千米長;它就在旁邊。然而,鑒于多數情況,遠程服務器請求和返回允許2秒,對于GPU的數據轉換的額外3~5毫秒會導致糟糕的動畫。
什么是GPU的有效載荷?多數情況下,它包含了層圖像,以及附加的說明比如大小,偏移量,以及動畫參數。下面大致的寫了有效負載及GPU傳輸的數據:
- 繪制每個合成層成獨立圖像。
- 準備層數據(例如大小,偏移量,透明度)
- 準備動畫的著色(如果用到的話)
- 發送GPU數據
如你所見,每當為元素添加transform: translateZ(0)
或者will-change: transform
,你會開始同樣的過程。而重繪是很耗性能成本的,這里它會更慢。多數情況下,瀏覽器不能進行遞增的重繪,它會去繪制之前覆蓋了新合成層的區域。
隱式合成(Implicit Compositing)
回到我們剛才A
和B
的例子。之前,我們動畫處于所有元素上層的A
,導致有兩個合成層:一是A
元素,另一個是B
元素和整個頁面背景(也就是沒有A
)。
現在,我們讓B
動畫。
我們陷入了邏輯問題。B
元素應該是一個獨立的合成層,最終的層圖像應該在GPU被合成。但是A
元素應該出現在B
的上面,我們并沒有定義關于A
的任何東西來推動它在自己層。
記住那個大的聲明:特殊的GPU-合成(GPU-compositing)模式并不是CSS規范的一部分;它只是瀏覽器內部應用的優化。因為定義了z-index
,A
肯定是在B
上方。而瀏覽器會做些什么呢?
它將會強制創建一個包含A
的新合成層,當然,添加了另一個重繪:
它被稱為隱式合成 implicit compositing:以堆疊順序應當出現在合成上的一個或多個非合成元素被提升為復合層 —— 即,被繪制為分離的圖像,然后將其發送到GPU。
我們在隱式合成里犯的錯遠比你想象的還要多。瀏覽器提升元素為合成層是有很多原因的,下面列了幾條:
- 3D變換:
translate3d
,translateZ
等; -
<video>
,<iframe>
元素; - 通過
Element.animate()
來改變transform
,opacity
- 通過css的transitions和animations改變
transform
,opacity
; -
position: fixed
; will-change
filter
可以看“CompositingReasons.h” 的文章,有更多關于谷歌瀏覽器的解釋。
看起來GPU動畫的主要問題似乎是意想不到的重繪,事實上并不是,最大的問題是……
內存消耗
再一次溫馨提醒,GPU是獨立式計算機:它不僅要將渲染的層圖像發送給GPU,而且要存儲它們便于在以后動畫的重用。
那么單個合成層需要多少內存?舉個例子,猜想下,保存一個填充色為#FF0000
的320*240的矩形,需多少內存。
典型的web開發者會去想,“這是一個純色圖。我會把它作為png來保存,再檢查大小,應該比1KB小。”毫無疑問,他們是正確的,這種圖片作為png是104字節(byte)。
問題是PNG,或JPEG,GIF等用來存儲以及傳輸圖像數據。為了將圖像繪制到顯示器上,計算機需要分析圖像格式,然后表示為像素數組/矩陣。所以,我們的示例圖片將會占320 × 240 × 3 = 230,400 bytes
的計算機內存。也就是說,我們要將圖像的寬乘高來獲取圖片的像素數。然后,我們再乘3,因為每個像素由3個字節(RGB)描述。如果圖像包含透明區域,我們需要乘4,因為需要額外的字節來描述透明度:(RGBa):320 × 240 × 4 = 307,200 bytes
。
瀏覽器總是將合成層繪制為RGBa圖像,看起來似乎沒有有效的方法來判斷元素是否包含透明區域。
舉個更可能的例子:10張圖片的輪播效果,每張800*600像素。我們需要在用戶交互(如拖動)時讓圖片之間進行平滑的切換,因此我們為每個圖片添加了will-change: transform
。這會事先將圖片提升為合成層,以便在用戶交互時立即轉換。如此一來,計算機顯示輪播圖需要的內存是: 800 × 600 × 4 × 10 ≈ 19 MB。
19MB的額外內存被用來渲染單個控件!如果你是現代web開發者,正在創建單頁面網站,并有很多動畫控件、視差效果、高分辨率圖像以及其他視覺增強,那么每頁額外的100~200MB才剛開始。添加隱式合成到混合(承認吧——你以前從沒想過這個),那你將會結束掉設備的所有可用內存。
此外,多數情況下,這些顯示相同結果的內存將會被浪費。
這對桌面客戶端來說可能不是一個問題,但它真的會損害移動端用戶。首先,現代的很多設備有高密度屏幕:合成層圖像的權重要乘4~9。其次,移動設備并沒有臺式機那么大的內存。例如,現在的iphone 6有1GB的共有內存(即內存既用于RAM,也用于VRAM)。考慮到至少1/3的內存被用于操作系統和后臺進程,另1/3被用于瀏覽器和現在的頁面(對于高度優化的沒有大量框架的頁面),我們最后會有大約200~300MB留給GPU效果。而iphone 6 是相當昂貴的高端設備;很多手機的內存會更少。
你可能會問,“在GPU存儲PNG圖片來減少內存空間可能嗎?”技術上說,有可能。問題是GPU是逐像素地繪制屏幕,意味著要將完整的PNG圖像解碼成一個一個的像素。我懷疑這種情況下的動畫比每秒1幀更快。
值得一提的是,針對GPU的 圖像壓縮格式是存在的,但是在壓縮比方面不如PNG或JPEG,而且功能會受硬件的影響。
優缺點
現在我們學到一些GPU動畫的基礎,一起總結下它的優缺點吧。
優點:
- 這種動畫更快更平滑,達到每秒60幀。
- 正確制作的動畫在單獨的線程運行,不會被JS的計算所阻塞。
- 3D轉換比較“廉價”。
缺點
- 額外的重繪將元素提升至合成層。有時候這是很慢的(即我們獲取整個層的重繪,而不是增量的部分)。
- 繪制層必須傳輸給GPU。根據層的數量和尺寸,傳輸可能會很慢,而導致中低端的設備產生閃爍現象。
- 每個合成層消耗額外的內存。而內存是移動端的寶貴資源,過度的內存使用會造成瀏覽器的崩潰。
- 如果不考慮隱式合成,緩慢地重繪,極有可能發生額外的的內存使用和瀏覽器崩潰。
- 我們會看到視覺失真,比如某些情況下Safari里渲染的文字和頁面內容消失或被扭曲。
如你所見,GPU動畫雖然有著實用獨特的優點,但也有不好的問題。其中最重要的是重繪和過度的內存使用;而下面涵蓋的所有優化技術將解決這些問題。
瀏覽器設置
在優化前,我們需要了解那些能幫助檢查頁面合成層,以及提供有關優化效果的明確反饋的工具。
SAFARI
Safari的web檢查器(Web Inspector)有個“layers”邊欄,來顯示所有合成層及內存消耗,合成原因。來看這個邊欄:
- 在Safari中,利用
? + ? + I
打開web檢查器,如果沒用,選擇左上角的“preferences”——> “Advanced” ,勾選“Show Develop Menu in menu bar”選項,然后重試。 - web檢查器打開后,選擇“Elements”選項,并在右側邊欄選擇“Layers”。
- 現在點擊“Elements”主面板的DOM節點,你將會看到選中元素的layers信息(如果它用了合成)以及派生的層。
- 單擊派生層查看合成原因。瀏覽器將告訴你為什么將該元素移動到自己的合成層。
(查看大圖)
CHROME
chrome的DevTools有類似的面板,但要先啟動標記:
- 在chrome中,前往
chrome://flags/#enable-devtools-experiments
,啟動 “Developer Tools experiments”(開發者工具實驗性功能) 的標記。 - Mac利用
? + ? + I
打開DevTools,PC利用Ctrl + Shift + I
,后點擊右上角的如下圖標,選擇“Settings”選項。 - 轉入“Experiments” 面板,勾選 “Layers”選項。
- 重新打開DevTools,你將看到“Layers”面板。
(查看大圖)
該面板將當前頁面的所有活動合成層顯示為樹。選擇某個層,你會看到相關信息如大小(size),內存消耗(memory consumption),重繪次數(repaint count)以及合成原因(reason for being composited)。
優化建議
已經設置好環境后,我們開始優化合成層。之前確定合成的主要兩個問題:額外的重繪(造成GPU的數據傳輸問題),以及額外的內存消耗。因此,下面的所有優化建議將針對上述問題:
避免隱式合成
這是最簡單也最重要的建議,是的,很重要。再次提醒,所有非合成的DOM元素帶有顯示合成原因(如position: fixed
, video,css animation)將會被強制提升為自己層,便于GPU的最終圖像合成。在移動端,這可能會導致動畫非常緩慢。
舉個例子(查看代碼鏈接,戳此進):
A
元素要在用戶交互時進行動畫。如果在“Layers”面板看這頁面,你會看到,它并沒有多余的層。而點擊“play”按鈕后,你會看到多層,這些圖層在動畫完成后立即刪除。如果在“TimeLine”面板看該過程,你會看到動畫的開始和結束都伴隨著大面積的重繪。
(查看大圖)
瀏覽器是這么一步步做的:
- 當頁面加載好后,瀏覽器若找不到合成原因,它會選取最佳策略:在單個背景層上繪制頁面所有內容。
- 當點擊“play”按鈕時,我們顯然看到增加了元素
A
的合成——因為transform
屬性。當瀏覽器確定堆疊順序的元素A
是在元素B
的下面,所以它提升B
為自己的合成層(隱式合成)。 - 提升至合成層總會造成重繪:瀏覽器必須為元素創建新的紋理,并將其從之前的層刪除。
- 新的圖層必須傳輸給GPU,便于用戶在屏幕上看到最終的圖像合成。根據層數,紋理大小和內容復雜度,需大量時間來執行重繪和數據傳輸。這也就是為什么我們有時會看到動畫開始或結束時元素在閃爍。
- 動畫完成后,我們去除了元素
A
合成的原因,那么,瀏覽器看到已經不需要合成了,就會回退到最佳策略:頁面所有內容都在一個層,這也就意味著背景層需重新繪制A
和B
(另一個重繪),并將新的紋理發給GPU。上述的步驟也就導致了閃爍。
為了擺脫隱式合成問題和減少視覺差異,我建議以下方法:
- 試著用
z-index
將動畫的元素保持盡可能高。理論上,這些元素應該是body
的直接子元素。當然,當動畫元素嵌套在DOM樹內且依賴正常流,這標記并不總是可能的。這種情況下,你可以克隆元素并將其放在body中僅用于動畫。 - 你可以給瀏覽器一個
wiil-change
的提示,表示準備合成。設置元素該屬性,瀏覽器將(但不總是)提前將其提升至合成層,以便動畫平滑的開始結束。但不要濫用該屬性,否則內存將大大增加!
動畫用TRANSFORM
和OPACITY
屬性
transform
和opacity
屬性保證既不影響正常流,也不影響DOM環境(即,不會造成回流或重繪,動畫可以完全轉移到GPU)。基本上,這意味著你可以有效的處理動畫移動,縮放,旋轉,透明度,以及變換。有時你可能想用這些屬性模仿其他動畫類型。
舉個簡單的例子:背景顏色的過渡。基本方法是添加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>
這種情況下,動畫完全在CPU上運行,每一步都會重繪。而我們可以在GPU上實現同樣的效果:取代background-color
屬性,我們在頂部添加一個層來變化它的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>
這個動畫更快更平滑,但是記住它會導致隱式合成和需要額外內存。這種情況大大減少內存消耗。
減少合成層的大小
看下面的圖片,有發現不同么?
這兩個合成層視覺上是一樣的,但第一個40000字節(39KB),第二個才400字節,少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=40000字節),而#b
只有10*10像素大小(10*10*4=400字節),利用transform: scale(10)
放大成100*100像素。#b
是合成層,由于帶了will-change
屬性,在最終圖像繪制期間,transform
完全是在GPU上發生。
技巧很簡單:利用width
和height
屬性減少物理大小,利用transform: scale(…)
升級其紋理。當然,對于非常簡單的純色層來說,這個技巧極大地減少了內存的消耗。舉個例子,如果你想動畫一張大照片,你可以縮小它到5%到10%,然后放大它;用戶可能看不出任何差別,你也節省出幾兆的寶貴內存。
如果可以的話,利用CSS的transitions和animations
我們已經知道,通過transform
以及opacity
會自動創建合成層,并在GPU上運行。我們同樣可以通過JavaScript來動畫,但需要添加transform: translateZ(0)
或will-change: transform, opacity
來保證元素獲得自己的合成層。
requestAnimationFrame
回調計算每個步驟,發生JavaScript動畫,通過Element.animate()
是一個有效的css動畫聲明。
一方面,通過css的transition或animation來創建簡單重用的動畫是很容易的;而另一方面,JS創建復雜的動畫比css簡單的多。此外,JavaScript是與用戶交互的唯一的路徑。
哪種方式更好?我們可以利用通用JavaScript庫來動畫元素么?
基于CSS的動畫有個重要的特征:它是完全在GPU上運行的。因為你聲明了動畫應該怎么開始和結束,瀏覽器可以在動畫開始前準備所有命令,并發給GPU。在命令式JavaScript的情況下,瀏覽器需要當前所有幀的狀態。為了實現平滑的動畫,我們需要在主瀏覽器線程計算新幀,然后每秒發送給GPU至少60次。除了計算和發送數據比css慢的多的事實外,它們還依賴于主進程的工作負載。
上述的范例里,你可以當主進程會被強化的JavaScript計算阻塞時會發生什么,而css動畫不會被影響,因為新幀是在單獨的線程里計算的,但JavaScript的動畫需要等大量計算完成后才計算新幀。
因此,盡可能使用基于css的動畫,特別是加載和進度指示,不僅更快,而且不會被大量JavaScript計算所阻塞。
現實的優化實例
這篇文章是我在開發 Chaos Fighters頁面過程中調查和試驗的結果。這是一個有著很多動畫的手機游戲的響應推廣頁面。當開始開發時,我只知道如何產生基于GPU的動畫,但并不知道它的工作原理。結果,第一個里程碑頁面導致iphone5 —— 當時最新的Apple手機——在加載完頁面后幾秒內崩潰。而現在,即使是不太高級的手機,這個頁面依然正常運行。
一起考慮這個頁面的有趣優化。
頁面最頂部是游戲介紹,類似紅色的光線在背景中旋轉。毫無疑問是個無限循環,沒有交互,一個很好的css動畫范例。第一個(誤導)的嘗試是保存光線圖像作為img
元素置于頁面上,并使用無限的css動畫。鏈接:[http://codepen.io/sergeche/pen/gwBjqG]
一切都看起來很正常,但是光線的圖片是很大的,移動端用戶并不會高興。
仔細觀察圖像。基本上,它只是來自圖像中心的幾條光線,而光線是相同的,所以我們可以保存單個光線圖像,并反復利用達到最終效果。最終得到單光線圖像,這遠比初始圖小的多。
針對這種優化,我們必須將.sun
的標記復雜化,它是光線圖像元素的容器。每一光線都有特定的旋轉角度。(代碼鏈接)[http://codepen.io/sergeche/pen/qaJraq]
視覺效果一樣,但網絡傳輸的數據量會少很多。合成層的尺寸都為500×500×4≈977KB。
弄的簡單些,示例的光線圖片很小,只有500*500像素。在真實的網站,設備的大小及像素分辨率并不相同(手機,平板,電腦),最終的圖片大約是3000*3000*4=36MB!而這僅僅是頁面上的一個動畫元素。
再看下“Layers”面板的頁面元素。我們已經讓整個太陽旋轉變得簡單。因此,這個容器會被提升至合成層,被繪制成單一的大紋理圖像,然后發給CPU。正因為我們的簡化,紋理中包含了無用的數據:之間的縫隙。
更多來說,無用的數據比有用的還多!占據有限的內存資源并不是一個最好的方式。
這個問題的解決方案跟網絡傳輸的優化相同:發送有用的數據(即光線)給GPU,我們可以計算節約了多大內存:
- 太陽容器:500*500*4 = 977KB
- 12條線: 250*40*4*12 = 469KB
內存消耗減少2倍。要做到這一點,我們分別動畫每條線,替換整個容器。這樣一來,只有光線圖片會被發給GPU,之間的間隙不會占據任何資源。
我們不得不使標簽復雜,以便單獨對光線進行動畫處理,而css的干擾也會更多。我們已經對線條初始旋轉動畫用了transform
,然后開始每個動畫一樣的效果,旋轉360度。基本上,我們需要創建一個單獨的@keyframes
部分,有很多傳輸的代碼。
編寫一個簡短的JavaScript來處理光線初始放置,并允許對動畫,光線數量等進行微調,這將變得更容易。見代碼 [http://codepen.io/sergeche/pen/bwmxoz]
新動畫看起來跟之前一樣,但內存消耗上少了2倍。
而且,在布局組成上,動畫的太陽不是主要元素,而是背景元素。光線沒有清晰的對比元素。這意味著,我們可以發略低分辨的光線圖給GPU,隨后將其升級,這幫我們減少一點內存消耗。
嘗試將紋理大小減小10%。光線的物理大小是250*0.9*40*0.9=255*36像素。為了使光線看起來像250*20,我們將其放大250÷225≈1.111。
我們將添加一行代碼background-size: cover
給.sun-ray
,便于背景圖片自動調整,然后添加transform: scale(1.111)
給光線。代碼http://codepen.io/sergeche/pen/YGJOva
注意,我們只改變了元素的大小; PNG圖像的大小保持不變。由DOM元素創建的矩形將作為GPU的紋理,而不是PNG圖像。
太陽光線在GPU的合成大小是225×36×4×12≈380 KB(之前是469KB)。我們減少了大概19%的內存,并獲得更靈活的代碼,通過縮減來得到更佳質量——內存比。因此,增加簡單動畫的復雜性,減少了977÷380≈2.5倍的內存!
我想你已經注意到,這個解決方案有個重大的缺點:動畫運行在CPU上會被JavaScript計算阻塞。如果你想更熟悉GPU操作動畫,我提個作業。codepen上fork下Codepen of the sun rays,使其完全運行在GPU上,就像先前的例子一樣高效靈活。在評論中發布你的代碼以獲得反饋。
收獲
對于Chaos Fighters 頁面的優化使我重新思考開發現代網頁的過程。這里列了幾條我的主要原則:
- 始終與客戶,設計談論網站上的所有動畫和效果。這會大大影響頁面的標簽,以為更好的合成。
- 一開始注意合成層的數量和大小,特別是隱式合成層。瀏覽器的開發工具中的“Layers”面板是你最好的伙伴。
- 現代瀏覽器頻繁使用合成,不僅用于動畫,而且優化頁面元素繪制。舉個例子,
position: fixed
和iframe
,video
使用合成。 - 合成層的大小比數量更重要。某些情況下,瀏覽器會嘗試減少合成層的數量(查看“GPU Accelerated Compositing in Chrome“的 “Layer Squashing”這一塊),它防止了“層爆炸”并減少內存消耗,特別是層有巨大的交叉點時。有時候,這種優化具有負面影響,比如說一個大的紋理比幾個小的層消耗更多內存。為了繞過這個優化,我給了
translateZ()
很小的值,比如說translateZ(0.0001px)
,translateZ(0.0002px)
。瀏覽器將確定元素位于3D空間的不同面板,因此跳過優化。 - 你不能僅添加
transform: translateZ(0)
或will-change: transform
給任意元素,來虛擬提高動畫性能或擺脫視覺差。GPU的合成要考慮弊端和取舍。當不使用時,合成會降低整體性能,最壞的情況導致瀏覽器崩潰。
請允許我再次提醒:這不是GPU合成的官方規范,每個瀏覽器解決同一問題方式是不同的。本文某些內容在幾個月后可能就過時了。例如,谷歌開發者正在探索如何減少CPU到GPU數據傳輸的開銷,包括零復制開銷的特殊共享內存的使用。此外,Safari已經能夠將簡單元素的繪制(比如說有background-color
的空DOM元素)委托給GPU,而不是在CPU上創建圖像。
無論如何,我希望這篇文章能幫助你更好地理解瀏覽器是如何使用GPU渲染的,以便您創建能在各設備下快速運行的令人印象深刻的網站了。
###詞匯介紹:
1. 紋理(texture)?
這里的紋理是 GPU 的一個術語:可以把它想象成一個從主存儲器(例如 RAM)移動到圖像存儲器(例如 GPU 中的 VRAM)的位圖圖像(bitmap image)。一旦它被移動到 GPU 中,你可以將它匹配成一個網格幾何體(mesh geometry),在 Chrome 中使用紋理來從 GPU 上獲得大塊的頁面內容。[參考源自http://web.jobbole.com/85993/]
2. 回流(reflow)
當渲染樹(render Tree)中的一部分(或全部)因為元素的規模尺寸,布局,隱藏等改變而需要重新構建。這就稱為回流(reflow),也就是重新布局(relayout)。
每個頁面至少需要一次回流,就是在頁面第一次加載的時候。在回流的時候,瀏覽器會使渲染樹中受到影響的部分失效,并重新構造這部分渲染樹,完成回流后,瀏覽器會重新繪制受影響的部分到屏幕中,該過程成為重繪。[參考源自http://web.jobbole.com/85993/]
3. 重繪(repaint)
當render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀,風格,而不會影響布局的,比如 background-color 。則就叫稱為重繪。
值得注意的是,回流必將引起重繪,而重繪不一定會引起回流。
明顯,回流的代價更大,簡單而言,當操作元素會使元素修改它的大小或位置,那么就會發生回流。[參考源自http://web.jobbole.com/85993/]
4. 亞像素精度(subpixel precision)
亞像素精度是指相鄰兩像素之間細分情況。輸入值通常為二分之一,三分之一或四分之一。這意味著每個像素將被分為更小的單元從而對這些更小的單元實施插值算法。例如,如果選擇四分之一,就相當于每個像素在橫向和縱向上都被當作四個像素來計算。因此,如果一張5x5像素的圖像選擇了四分之一的亞像素精度之后,就等于創建了一張20x20的離散點陣,進而對該點陣進行插值。[來自百度百科]