這樣使用 GPU 渲染 CSS 動畫(轉)

大多數人知道現代網絡瀏覽器使用GPU來渲染部分網頁,特別是具備動畫的部分。 例如,使用transform屬性的CSS動畫看起來比使用lefttop屬性的動畫更平滑。 可是若是你問,「我如何從GPU得到平滑的動畫?」在大多數狀況下,你會聽到像「使用transform:translateZ(0)will-change:transform這樣的建議。html

這些屬性已經成爲像咱們如何在Internet Explorer 6下使用zoom:1(若是你明白個人意思的話)在準備GPU的動畫或說合成加速,瀏覽器廠商喜歡這樣叫它。git

但有時,在簡單demo中運行的又好又平滑的動畫,放在一個真實的網站上運行的時候卻很慢,會形成視覺假象,甚至致使瀏覽器崩潰。 爲何會發生這種狀況?** 咱們如何解決它?** 讓咱們試着去了解。github

一個免責聲明

在咱們深刻GPU加速以前,我想告訴你最重要的事:這是一個 giant hack。 你不會在(至少如今)W3C的規範中找到任何關於合成加速是如何運做,關於如何在合成層上顯式地放置一個元素,甚至是關於合成加速自己。 它只是瀏覽器應用在執行某些任務時的優化,並且各個瀏覽器廠商都經過本身的方式去實現這種優化。web

在本文中你將學到的一切並非對合成加速是如何運做的官方解釋,而是我用本身的一些常識和不一樣瀏覽器系統工做原理的知識去實驗的結果。可能會有一些小錯誤,有些過段時間可能會改變 —— 我已經提醒過你了哦!chrome

合成加速是如何運做

要準備一個GPU動畫的頁面,咱們必須瞭解其在瀏覽器中如何工做,而不僅是隨便的去遵循從網上或從這篇文章中獲得的建議。數據庫

假設咱們有一個包含AB元素的頁面,每一個元素都有position:absolute和一個不一樣的z-index。 瀏覽器將從CPU繪製它,而後將生成的圖像發送到GPU,最後將顯示在屏幕上。canvas

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

咱們已經決定經過A元素的left屬性和CSS動畫來使其運動起來:瀏覽器

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

在這種狀況下,對於每一個動畫幀,瀏覽器必須從新計算元素的幾何形狀(即重排),和渲染頁面新狀態下的圖像(即重繪),而後再次發送到GPU將其顯示在屏幕上.咱們都知道從新繪製是很是耗性能的,但每一個現代瀏覽器都很是聰明地只重繪頁面中改變的區域,而不是整個頁面。 雖然瀏覽器在大多數狀況下能夠很是快速地重繪,但咱們的動畫仍然不夠平滑。緩存

在動畫的每一個步驟(甚至遞增)重排和重繪整個頁面聽起來真的很慢,特別是對於一個大且複雜的佈局。比較有效的方法是繪製兩個單獨的圖像 —— 一個用於A元素,一個用於沒有A元素的整個頁面 —— 而後簡單地讓這些圖片相對於彼此偏移,換句話說,合成緩存元素的圖像將會加速。 這正是GPU的亮點所在:它可以以亞像素精度快速構圖,爲動畫增添了平滑感。服務器

要優化合成,瀏覽器必須確保動畫的CSS屬性:

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

你們可能會認爲absolutefixedtop和 left屬性不依賴於元素環境,但事實並不是如此。例如,left屬性能夠接收取決於定位父級大小的百分比值; 一樣的,emvh和其餘單位取決於他們的環境。 相反,transformopacity是惟一知足上述條件的CSS屬性。

讓咱們經過transform來替換left實現動畫效果:

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

在這裏,咱們以聲明的方式描述了動畫:它的開始位置,結束位置,持續時間等。這會告訴瀏覽器提早更新CSS屬性。 由於瀏覽器沒有看到任何會致使重排或重繪的屬性,它能夠經過合成優化:將兩個圖像繪製爲合成圖層並將其發送到GPU。

這種優化的優勢是什麼?

  • 咱們能夠經過亞像素精度獲得一個運行在特殊優化過的單位圖形任務上的平滑動畫,而且運行很是快。
  • 動畫再也不綁定到CPU。 即便你運行一個很是複雜的JavaScript任務,動畫仍然會很快運行。

一切彷佛都很清楚和容易,對吧? 但咱們可能遇到什麼問題? 讓咱們看看這個優化是如何工做的.

GPU是一個單獨的計算機,這可能會讓你感到驚訝。但這是正確的:每一個現代設備的一個重要部分其實是一個獨立的單元,有本身的處理器和本身的內存和數據處理模型。 和任何其餘應用程序或遊戲同樣,瀏覽器須要與GPU交談。

爲了更好地瞭解這是如何工做的,想一想AJAX。 假設你想經過他們在網絡表單中輸入的數據去計算網站訪問者數量。 你不能只告訴遠程服務器,「嘿,從這些輸入字段和JavaScript變量中獲取數據並將其保存到數據庫。」遠程服務器不能訪問用戶瀏覽器中的內存。 相反,您必須將頁面中的數據收集後轉化爲可輕鬆解析的簡單數據格式(如JSON),並將其發送到遠程服務器。

在合成過程當中也會發生相似的狀況。 由於GPU就像一個遠程服務器,瀏覽器必須首先建立一個有效負載,而後將其發送到設備。 固然,GPU不是距離CPU幾千千米遠; 它就在那裏。 可是,儘管遠程服務器請求和響應所需的2s在多數狀況下是可接受的,可是一個GPU數據傳輸額外耗費的35毫秒將致使"janky"動畫。

什麼是GPU有效負載? 在大多數狀況下,它包括層圖像,以及它附加的數據,如圖層的大小,偏移量,動畫參數等。這裏的GPU有效負載和傳輸數據大體像是:

  • 將每一個合成圖層繪製爲單獨的圖像
  • 準備圖層數據(大小,偏移,不透明度等)
  • 準備動畫的着色器(若是適用)
  • 將數據發送到GPU

正如你能夠看到的,每次你給元素添加transform:translateZ(0)will-change:transform屬性,你都啓動了相同進程。 重繪成本是很是高昂的,運行甚至更慢。 在大多數狀況下,瀏覽器沒法增量重繪。 它必須用新建的複合層去繪製以前被覆蓋的區域:

隱式合成

讓咱們回到咱們的AB元素的例子。 以前,咱們讓A元素在頁面上其餘全部元素之上動起來了。 這致使有兩個合成層:一個是A元素所在的層和一個B元素所在的頁面背景層。 如今,讓咱們來讓B元素動起來:

咱們遇到了一個邏輯問題。 元素B應該在單獨的合成層上,而且屏幕的最終頁面圖像應該在GPU上組成。 可是A元素應該出如今元素B的頂部,並且咱們沒有指定任何關於提高A元素自身層級的東西。

請記住這個提醒:特殊的GPU合成模式不是CSS規範的一部分; 它只是一個瀏覽器在內部應用的優化。 咱們經過定義z-indexA必須按照順序出如今B的頂部。 那麼瀏覽器會作什麼?

你猜到了! 它將強制爲元素A建立一個新的合成圖層 — 並添加另外一個重繪圖,固然:

這被稱爲隱式合成:一個或多個非合成元素應該出如今層疊順序中被提高的複合層之上 —— 即繪製爲分離的圖像,而後發送到GPU。

咱們偶然發現隱式合成比你想象的更頻繁。 瀏覽器會將元素提高爲合成層的緣由有不少,其中包括:

  • 3D transforms: translate3dtranslateZ等等;
  • <video>,<canvas> 和 <iframe> 元素;
  • 經過Element.animate()而有transform動畫和opacity屬性的元素;
  • 經過СSS transitions 和 animations而有transform動畫和opacity屬性的元素;
  • position: fixed;
  • will-change;
  • filter;

更多緣由描述在Chromium項目的「CompositingReasons.h」文件中。

看起來GPU動畫的主要問題好像是意想不到的重繪。但其實否則,更大的問題實際上是...

內存消耗

另外一個溫柔的提醒,GPU是一個單獨的計算機:它不只須要發送渲染層圖像到GPU,並且還需存儲它們,以便稍後在動畫中重用。

單個複合層須要多少內存? 讓咱們舉個簡單的例子。 嘗試猜想須要多少內存來存儲一個用純#FF0000顏色填充的320 × 240px矩形。

一個典型的Web開發人員會認爲,「嗯,這是一個純色的圖像。 我將它保存爲PNG並檢查其大小。 它應該小於1KB。「他們是絕對正確的:這個圖像做爲PNG的大小是104字節。

問題是PNG,以及JPEG,GIF等,用於存儲和傳輸圖像數據。 爲了將這樣的圖像繪製到屏幕上,計算機必須解壓圖像格式,而後將其表示爲像素陣列。 所以,咱們的樣本圖像將須要320×240×3 = 230,400字節的計算機內存。 也就是說,咱們將圖像的寬度乘以其高度以得到圖像中的像素數。 而後再乘以3,由於每一個像素由三個字節(RGB)描述。 若是圖像包含透明區域,咱們將其乘以4,由於須要額外的字節來描述透明度:(RGBa):320×240×4 = 307,200字節

瀏覽器老是將合成圖層繪製爲RGBa圖像。 彷佛沒有有效的方法來肯定一個元素是否包含透明區域。

讓咱們舉一個可能的例子:一個輪播有10張照片,每張是800 × 600px。 咱們來實現一個圖片間平滑過渡的交互,好比拖拽,因而咱們爲每一個圖像添加了will-change:transform。 這將提早將圖像提高爲複合層,以便在用戶交互時當即開始過渡。 如今來計算下須要多少額外的內存來顯示這樣的輪播:800×600×4×10≈19MB

須要19 MB的額外內存來渲染單個控件! 若是你是一個在製做一個單頁面應用程序網站的現代Web開發人員的話,須要不少動畫控制,視差效果,高分辨率圖像和其餘視覺加強功能,那麼每頁額外100到200 MB 只是開始,還須要添加隱式合成去混合,你最終會以用盡設備上的可用內存結束。

此外,在許多狀況下,這個內存將被浪費,並顯示很是相同的結果:

對桌面客戶端用戶還好,但對於使用移動設備的用戶來講是很坑的。 首先,大多數現代設備具備高密度屏幕:將複合層圖像的權重乘以4到9。其次,移動設備沒有臺式機那麼多的內存。 例如,一個不是很舊的iPhone 6附帶1 GB的共享內存(即用於RAM和VRAM的內存)。 考慮到這個內存的至少三分之一被操做系統和後臺進程使用,另外三分之一被瀏覽器和當前頁面使用(高度優化的頁面沒有大量框架的最佳狀況),咱們最多還剩下大約200到300 MB內存給GPU效果。 iPhone 6是一個至關昂貴的高端設備;比它便宜的手機內存更少。

你可能會問,「有可能在GPU中存儲PNG圖像以減小內存佔用嗎?」從技術上來講,是的,這是可能的。 惟一的問題是GPU逐像素地繪製屏幕,這意味着它必須一次又一次地爲每一個像素解碼整個PNG圖像。 我懷疑在這種狀況下的動畫會比每秒1幀更快。

值得一提的是,GPU特定的圖像壓縮格式確實存在,可是它們在壓縮比方面甚至還比不上PNG或JPEG,而且它們的使用受硬件支持的限制。

優勢和缺點

如今咱們已經學習了一些GPU動畫的基礎知識,讓咱們總結它的優勢和缺點。

優勢

  • 動畫快速,流暢,每秒60幀。
  • 一個正確製做的動畫在單獨的線程中運做,而且不會被大量JavaScript計算阻止。
  • 3D變換是「便宜的」。

缺點

  • 添加劇繪是須要提高元素層級到複合層。 有時這是很是慢的(即咱們獲得一個全層重繪,而不是一個增量)。
  • 繪圖層必須傳輸到GPU。 根據這些層的數量和尺寸,轉移也可能很是慢。 這可能致使元素在低端和中端市場設備上閃爍。
  • 每一個複合層都消耗額外的內存。 內存是移動設備上的寶貴資源。 過多的內存使用可能會致使瀏覽器崩潰。
  • 若是你不考慮隱式合成,而使用慢速重繪,除了額外的內存使用,瀏覽器崩潰的概率也很是高。
  • 咱們會有視覺假象,例如在Safari中的文本渲染,在某些狀況下頁面內容將消失或變形。

正如你能夠看到,GPU動畫不只有一些很是有用和獨特的優點,也有一些很是討厭的問題。最主要是重畫和過分的內存佔用; 所以,下面涵蓋的全部優化技術都將解決這些嚴重的問題。

瀏覽器設置

在咱們開始優化,咱們須要瞭解將幫助咱們檢查頁面上的複合層,和提供關於優化效率反饋的工具。

SAFARI

Safari的Web Inspector有一個很棒的「Layers」邊欄,用來顯示全部複合層及其內存消耗,以及合成的緣由。 查看此側邊欄:

  • 在Safari中,使用⌘+⌥+ I打開Web Inspector。若是不起做用,請打開「Preferences」→「Advanced」,打開「Show Develop Menu in menu bar」選項,而後重試。
  • 當Web Inspector打開時,選擇「Elements」選項,而後在右側邊欄中選擇「Layers」。
  • 如今,當你單擊一個在主「Elements」窗中的DOM節點時,您將看到所選元素(若是使用合成)和全部後代複合圖層的圖層信息。
  • 單擊一個後代圖層以查看其合成的緣由。 瀏覽器會告訴你爲何將這個元素移動到本身的合成圖層上。

CHROME

Chrome的開發者工具備一個相似的面板,但你必須先啓用標誌:

  • 在Chrome中,前往chrome:// flags /#enable-devtools-experiments,並啓用"Developer Tools experiments"標記。
  • 使用⌘+⌥+ I(在Mac上)或Ctrl + Shift + I(在PC上)打開開發者工具,而後單擊右上角的圖標並選擇「Settings」菜單項。
  • 轉到「Experiments」窗格,而後啓用「Layers」面板。
  • 從新打開開發者工具。 你如今應該看到「Layers」面板。

此面板將當前頁面的全部活動合成圖層顯示爲樹。 選擇圖層時,您將看到相應的信息,例如其大小,內存消耗,重繪數量和合成緣由。

優化技巧

如今咱們已經設置了咱們的環境,咱們能夠開始優化合成層。 咱們已經肯定了合成的兩個主要問題:額外的重繪,這也會使數據傳輸到GPU,以及額外的內存消耗。 所以,下面全部的優化技巧都主要針對這個問題。

避免隱式合成

這是最簡單和最顯而易見的技巧,也是很是重要的技巧。 讓我提醒你,全部非合成的DOM元素具備顯式合成緣由(例如, position: fixedvideo,CSS動畫等))將被強制提高到本身的圖層,只是爲了在GPU上合成最後的圖像。 在移動設備上,這可能會致使動畫開始很是緩慢。

讓咱們舉個簡單的例子:

A元素會在用戶交互時動起來。 若是你在「Layers」面板中查看此頁面,你看不到額外的圖層。 可是在點擊「播放」按鈕後,你會看到更多的圖層,這些圖層將在動畫完成後當即刪除。 若是你在「Timeline」面板中查看此過程,你會看到動畫的開始和結束進行了大面積的重繪:

瀏覽器作了如下幾步:

  • 在頁面加載後,瀏覽器找不到任何合成理由,所以它選擇最佳策略:在單個背景圖層上繪製頁面的整個內容。
  • 經過點擊「播放」按鈕,咱們明確地添加了合成給元素A —— 一個具備transform屬性的過渡動畫。 可是瀏覽器肯定元素A在層疊順序中低於元素B,因此它也將B提高到本身的合成層(隱式合成)。
  • 提高到合成層老是會致使重繪:瀏覽器必須爲元素建立新的紋理,並將其從上一層中刪除。
  • 新圖層必須傳輸到GPU,以便用戶在屏幕上看到的最終圖像合成。 根據層數,紋理的大小和內容的複雜性,從新繪製和數據傳輸可能須要大量的時間來執行。 這就是爲何咱們有時會看到一個元素在動畫開始或結束的時候閃爍。
  • 在動畫完成後,咱們從A元素中刪除合成的理由。瀏覽器看到它不須要浪費資源去合成,因此它回到最佳策略:保持頁面的整個內容在一個單一的層,這意味着它必須在背景上繪製AB層(另外一個重繪),並將更新的紋理髮送到GPU。 如上面的步驟,這可能會致使閃爍。

爲了擺脫隱式合成問題和減小視覺假象,我建議以下:

  • 儘量在z-index中保持動畫對象。 理想狀況下,這些元素應該是body元素的直接子元素。 固然,當動畫元素嵌套在DOM樹內部而且依賴於正常流時,這在標記中是不必定的。 在這種狀況下,您能夠克隆元素並將其放在body中僅用於動畫。
  • 你能夠給瀏覽器一個提示,你將要去合成使用與具備will-changeCSS屬性的元素。 經過在元素上設置此屬性,瀏覽器將(但不老是)提早將其提高到合成層,以便動畫能夠平滑地開始和中止。 可是不要濫用這個屬性,不然你的內存消耗會大大增長!

只有動畫TRANSFORM 和 OPACITY屬性

transformopacity屬性保證既不影響也不受正常流或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屬性,咱們在頂部添加一個圖層和給它的不透明度添加動畫:

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

這個動畫會更快更流暢,但請記住,它可能致使隱式合成,並須要額外的內存。 但在這種狀況下,能夠大大減小存儲器消耗。

減少複合層的尺寸

看看下面的圖片。 注意任何差別?

這兩個複合層在視覺上是相同的,但第一個重40,000字節(39 KB),第二個只有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×100px100×100×4 = 40,000字節),而#b只有10×10px10×10×4 = 400字節), 使用transform:scale(10)縮放到100×100px。 由於#b是一個複合層,因爲will-change屬性,`transform'會在最終圖像繪製期間徹底出如今GPU上。

這個訣竅很簡單:使用widthheight屬性減小複合層的物理大小,而後使用transform:scale(...)'擴展它的紋理。固然,這個技巧只簡單粗暴地減小了實色層的內存消耗。若是你想讓一張大照片動起來,你能夠把它縮小5%10%`,而後把它縮放一級; 用戶可能看不到任何差別,你還將節省幾兆字節的寶貴內存。

儘量使用 CSS TRANSITIONS和動畫

咱們已經知道動畫的transformopacity是經過CSS transitions 或animations 自動建立一個合成層,並在GPU上工做。 咱們也能夠經過JavaScript添加動畫,可是咱們必須首先添加transform:translateZ(0)will-change:transform,`opacity',以確保元素得到本身的合成層。

JavaScript animation happens when each step is manually calculated in a requestAnimationFrame callback. Animation via Element.animate() is a variation of declarative CSS animation.

JavaScript動畫發生在requestAnimationFrame的每一次回調手動計算時。 經過「Element.animate()」實現的動畫是變量聲明的CSS動畫變體。

一方面,經過CSS transition 或animation 建立一個簡單且可重用的動畫是很容易的; 另外一方面,在建立複雜的動畫時,使用JavaScript動畫比使用CSS動畫更容易。 此外,JavaScript是與用戶輸入交互的惟一方式。

哪個更好? 咱們能夠只使用一個通用JavaScript庫來實現一切動畫嗎?

基於CSS的動畫有一個很是重要的功能:它徹底在GPU上工做。 由於你聲明瞭動畫應該如何開始和結束,瀏覽器能夠在動畫開始以前準備好全部須要的指令,並將它們發送到GPU。 而在JavaScript的狀況下,瀏覽器要確認全部當前幀的狀態。 爲了平滑的動畫,咱們必須在主瀏覽器線程中計算新幀,而且將其發送到GPU每秒至少60次。 除了計算和發送數據比CSS動畫慢得多外,它們還依賴於主線程的工做負載:

在上面的圖中,你能夠看到當主線程被密集的JavaScript計算阻塞時會發生什麼。 然而CSS動畫是不受影響的,由於新幀是在單獨的線程中計算的,而JavaScript動畫必須等待大量計算完成,而後再計算新的幀。

因此,儘可能使用基於CSS的動畫,特別是加載和進度指示條。由於它不只是快,並且不會被大量的JavaScript計算阻止。

一個優化實例

本文是關於Chaos Fighters網頁的調查和實驗開發結果, 這是一個有着不少動畫的手機遊戲促銷頁面。 當我開始開發時,我只知道如何製做基於GPU的動畫,但我不知道它的工做原理。 結果,第一個里程碑式的頁面致使iPhone 5 —— 當時最新的蘋果手機 —— 在頁面加載後幾秒鐘內崩潰。 如今這個頁面能夠在即便不是那麼強大的設備上正常運行。

我認爲,咱們應該要考慮下這個網站的有趣優化。

在頁面開始是遊戲的介紹,有相似紅色光線在背景中旋轉, 它是一個無限循環、非交互式旋轉器—是簡易CSS動畫的最佳選擇。 第一個(誤導)嘗試是保存太陽光線的圖像,將其做爲img元素放在頁面上,並使用無限CSS動畫:

看起來貌似沒有問題。 可是太陽圖片很是大。 移動用戶使用起來會很不高興。

仔細看看圖像,基本上它只是來自圖像中心的幾條光線。 光線是相同的,因此咱們能夠保存單個光線的圖像,並從新利用它來建立最終的圖像.最終咱們將獲得比初始圖像小一個數量級的單射線圖像。

對於這種優化,咱們必須使標記複雜化:.sun將是一個元素與射線圖像的容器。 每一個射線將以特定角度旋轉。

html, body {
  overflow: hidden;
  background: #a02615;
  padding: 0;
  margin: 0;
}

.sun {
  position: absolute;
  top: -75px;
  left: -75px;
  width: 500px;
  height: 500px;
  animation: sun-spin 10s linear infinite;
}

.sun-ray {
  width: 250px;
  height: 40px;
  background: url(//sergeche.github.io/gpu-article-assets/images/ray.png) no-repeat;

  /* align rays with sun center */
  position: absolute;
  left: 50%;
  top: 50%;
  margin-top: -20px;
  transform-origin: 0 50%;
}

$rays: 12;
$step: 360 / $rays;

@for $i from 1 through $rays {
  .sun-ray:nth-of-type(#{$i}) { transform: rotate(#{($i - 1) * $step}deg); }
}

@keyframes sun-spin {
  from { transform: rotate(0); }
  to   { transform: rotate(360deg); }
}

視覺結果將是相同的,但網絡傳輸的數據量將更低。然而,複合層的尺寸保持相同:500×500×4≈977KB

爲了達到簡化,咱們的例子中的太陽光線至關小,只有500×500像素。 在真實的網站上,投放不一樣尺寸(移動,平板電腦和臺式機)和像素密度的設備,最終獲得的圖片大約是3000×3000×4 = 36 MB! 而這只是頁面上的一個動畫元素。

在「圖層」面板中再次查看網頁的標記。 咱們能夠更容易旋轉整個太陽容器。 所以,這個容器被提高爲一個合成層,並被繪製成一個單一的大紋理圖像,而後發送到GPU。 可是因爲咱們的簡化,紋理如今包含無用的數據,即光線之間的間隙。

此外,無用的數據在大小上比有用的數據大得多! 但這不是咱們合理利用內存資源的最好方式。

這個問題的解決方案與咱們網絡傳輸的優化相同:僅將有用數據(即光線)發送到GPU。 咱們能夠計算出咱們要保存的內存量:

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

內存消耗將減小兩倍。 要作到這一點,咱們必須將每一個射線的動畫分開,而不是動畫的容器。 所以,只有光線的圖像將被髮送到GPU; 它們之間的差距不會佔用任何資源。

咱們必須使咱們的標記複雜化,以便獨立地對光線進行動畫處理,此時CSS將成爲障礙。 咱們已經對光線的初始旋轉使用了transform,並且咱們必須從徹底相同的角度開始動畫,並進行360deg轉動。 基本上,咱們必須爲每一個射線建立一個單獨的@keyframes部分,這是不少網絡傳輸的代碼。

編寫一個簡短的JavaScript來處理光線的初始放置,而且容許咱們對動畫,光線數量等進行微調.

const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const rays = createRays(container, raysAmount);

animate();

function animate() {
    rays.forEach(ray => {
        ray.angle += angularVelocity;
        ray.elem.style.transform = `rotate(${ray.angle % 360}deg)`;
    });
    requestAnimationFrame(animate);
}

function createRays(container, amount) {
    const rays = [];
    const rotationStep = 360 / amount;
    while (amount--) {
        const angle = rotationStep * amount;
        const elem = document.createElement('div');
        elem.className = 'sun-ray';
        container.appendChild(elem);
        rays.push({elem, angle});
    }
    return rays;
}

新動畫看起來與前一個相同,可是實際上比上一個少了2倍的內存消耗。

不只僅是這樣, 在佈局組成方面,這個動畫太陽不是主要元素,而是一個背景元素。 光線沒有任何清晰的對比元素。 這意味着咱們能夠向GPU發送較低分辨率的光線紋理並隨後將其升級,這使得咱們減小了一點內存消耗。

讓咱們嘗試將紋理的大小減小10%。 光線的物理尺寸將爲250×0.9×40×0.9 = 225×36像素。 爲了使光線看起來像250×20,咱們必須將它升級250 ÷ 225 ≈ 1.111.

咱們將爲咱們的代碼添加一行代碼 —— 給.sun-ray添加background-size:cover—以便背景圖片自動調整爲元素的大小,咱們將爲射線的動畫添加transform: scale(1.111)

const container = document.querySelector('.sun');
const raysAmount = 12;
const angularVelocity = 0.5;
const downscale = 0.1;
const rays = createRays(container, raysAmount, downscale);

animate();

function animate() {
    rays.forEach(ray => {
        ray.angle += angularVelocity;
        ray.elem.style.transform = `rotate(${ray.angle % 360}deg) scale(${ray.scale})`;
    });
    requestAnimationFrame(animate);
}

function createRays(container, amount, downscale) {
    const rays = [];
    const rotationStep = 360 / amount;
    while (amount--) {
        const angle = rotationStep * amount;
        const elem = document.createElement('div');
        elem.className = 'sun-ray';
        container.appendChild(elem);

        let scale = 1;
        if (downscale) {
            const origWidth = elem.offsetWidth, origHeight = elem.offsetHeight;
            const width = origWidth * (1 - downscale);
            const height = origHeight * (1 - downscale);
            elem.style.width = width + 'px';
            elem.style.height = height + 'px';
            scale = origWidth / width;
        }

        rays.push({elem, angle, scale});
    }
    return rays;
}

注意,咱們只改變了元素的大小; PNG圖像的大小保持不變。 由DOM元素建立的矩形將呈現爲GPU的紋理,而不是PNG圖像。

太陽射線在GPU上的新組成大小如今是225×36×4×12≈380 KB(它是469 KB)。 咱們將內存消耗下降了19%,而且獲得了很是靈活的代碼,咱們能夠經過縮減來得到最佳的質量 - 內存比。 所以,經過增長動畫的複雜性,看起來這麼簡單,咱們已經減小了內存消耗977 ÷ 380≈2.5倍!

我想你已經注意到這個解決方案有一個重大的缺陷:動畫如今在CPU上工做,並且會被大量JavaScript計算阻止。 若是你想更熟悉如何優化GPU動畫,我留個小做業。在這個demo中 Codepen of the sun rays,讓太陽射線動畫徹底在GPU上工做,還要保證像原來的例子中的內存效率和彈性。 在評論中發佈你的示例以獲取反饋。

課程學習

  • 優化 Chaos Fighters頁面的研究使我徹底從新思考現代網頁的開發過程。 如下是個人主要原則:
  • 始終與客戶和設計師討論網站上的全部動畫和效果。 它會很大程度上影響頁面的標記,以便於更好的合成。
  • 從一開始就注意複合層的數量和大小 —— 特別是經過隱式合成建立的層。 瀏覽器開發工具中的「Layers」面板是你最好的朋友。
  • 現代瀏覽器不只將合成大量地用於動畫,並且還用於優化繪製頁面元素。 例如,position:fixediframevideo元素使用合成。
  • 合成層的尺寸可能比層的數量更重要。 在某些狀況下,瀏覽器會嘗試減小複合層的數量(請參閱「GPU加速複合在Chrome中的」圖層壓縮「部分); 這防止了所謂的「層爆炸」而且減小了存儲器消耗,特別是當層具備大的交叉點時。 可是有時,這種優化具備負面影響,例如當很是大的紋理比幾個小的層消耗更多的存儲器時。 爲了繞過這個優化,我向每一個元素添加一個小的,惟一的translateZ()值,例如translateZ(0.0001px)translateZ(0.0002px)等。瀏覽器將肯定元素位於3D空間中的不一樣平面 並所以跳過優化。
  • 你不能只靠爲任何隨機元素添加transform:translateZ(0)will-change:transform,來虛擬地提升動畫性能或擺脫視覺假象。 GPU合成有許多缺點和要權衡的地方。 當不使用時,合成會下降總體性能,甚至會致使瀏覽器崩潰。

請容許我提醒你們:GPU合成沒有官方規範,每一個瀏覽器解決的問題也不一樣。 本文的某些部分可能在幾個月後就過期了。 例如,Google Chrome開發人員正在探索如何減小CPU到GPU數據傳輸的開銷,包括使用零複製開銷的特殊共享內存。 而且Safari已經可以將簡單元素(例若有background-color的空DOM元素)的繪圖委託給GPU,而不是在CPU上建立它的圖像。

相關文章
相關標籤/搜索