從瀏覽器渲染原理,說一說如何實現高效的動畫

寫在前面

在平時的工做中,頁面的動畫效果是很常見的需求。那麼,怎麼樣實現一個高效的動畫呢?javascript

本文首發於公衆號:符合預期的CoyPan

注,本文談到的瀏覽器,均爲基於Chromium的現代瀏覽器。css

頁面渲染原理

圖片描述

一個頁面展現在用戶面前,簡單來講,會經歷以上5個步驟。咱們能夠把上面這個圖稱爲像素管道html

  • Javascript: 執行js邏輯,修改DOM,修改CSS等。
  • Style:計算樣式。
  • Layout:在知道對一個元素應用哪些規則以後,瀏覽器便可開始計算它要佔據的空間大小及其在屏幕的位置。這個步驟,就是咱們常說的重排。
  • Paint: 繪製是填充像素的過程。它涉及繪出文本、顏色、圖像、邊框和陰影,基本上包括元素的每一個可視部分。繪製通常是在多個表面(一般稱爲層)上完成的。這個步驟,就是咱們常說的重繪。
  • Composite:渲染層合併,由上一步可知,對頁面中 DOM 元素的繪製是在多個層上進行的。在每一個層上完成繪製過程以後,瀏覽器會將全部層按照合理的順序合併成一個圖層,而後顯示在屏幕上。

在瀏覽器中,頁面的渲染由瀏覽器的渲染進程完成,而渲染進程中,包含了主線程,worker線程,Compositer線程,Raster線程。上述像素管道的5個過程當中,前4個過程,都由主線程完成,最後一個步驟,主要由Raster線程、Compositer線程完成java

JavaScript、Style、Layout

像素管道中的前三個步驟,你們都很熟悉了。JavaScript、Style兩個步驟,一圖以蔽之:
圖片描述css3

接着是Layout,瀏覽器遍歷render tree的每個節點,計算其確切大小和位置。最終造成一個Layout Tree。
圖片描述web

Paint

在Paint以前,瀏覽器會根據Layout Tree,肯定須要繪製的對象的層級,咱們能夠把這個層級叫作渲染層,最終生成Layer Tree。這個階段被稱做:Update Layer Treecanvas

圖片描述

在Paint這個階段,瀏覽器會根據Layer Tree,生成Paint Records。瀏覽器

Paint Records就是描述先畫什麼,再畫什麼的記錄,跟咱們寫canvas代碼時很像。Paint Records是根據渲染層劃分的的。來看一個Paint Records的實例:css3動畫

圖片描述

儘管生成了Pain Records,真正的繪製並不在Paint這個階段完成的,而是在Composite階段由Raster線程完成的。網絡

Composite

通過以前的幾個步驟,瀏覽器主線程已經將頁面的內容分紅了若干渲染層。爲了提高性能,某些特定的渲染層,會被提高爲合成層。咱們能夠經過下面兩個css屬性,將某個元素強制提高爲合成層:

will-change: transform;

// 或者
transform: translateZ(0);
注:提高爲合成層的條件比較複雜,這裏就不一一展開了。能夠參考這篇文章:

http://taobaofed.org/blog/201...

主線程在處理完全部的全部的數據後,會把數據提交到Compositer線程。Composite線程會利用Raster線程來作光柵化處理,並將處理好的內容存入內存中。隨着Composite線程完成渲染層合成操做,扔給GPU,頁面最終被渲染到屏幕上。

圖片描述

能夠經過Chrome開發者工具中的Layer來查看合成層:
圖片描述

其餘運行方式的像素管道

上文中的像素管道共有5個步驟。不必定每幀都老是會通過管道每一個部分的處理。實際上,不論是使用 JavaScript、CSS 仍是網絡動畫,在實現視覺變化時,管道針對指定幀的運行還有其餘兩種方式:
圖片描述
圖片描述

第一種就是咱們所說的頁面沒有進行重排,只進行了重繪;第二種就是頁面既沒有進行重排,也沒有進行重繪。

最後這種運行方式的開銷最小,適合於頁面上的動畫效果。

實現動畫效果

不考慮canvas等,有三種常見的方式來實現頁面上的動效,

  1. 徹底不用css3相關屬性,僅使用setTimeout, setInterval, requestAnimationFrame,經過js修改DOM的樣式來實現動畫。
  2. 使用純css3來實現動畫。
  3. js與css3相結合來實現動畫。

通常狀況下,使用第一種方式的時候,雖然有的動畫效果在進行過程當中不會觸發像素管道中的Layout,可是Paint每每是避免不了的。而使用css3來實現動畫時,咱們能夠跳過Layout和Paint步驟。

下面,來看看三種實現方式下,瀏覽器的處理過程。

徹底由JS驅動的動畫

代碼以下:

<html>
    <head>
        <style type="text/css">
            #test2 {
                margin-top: 100px;
                width: 100px;
                height: 100px;
                position: relative;
                background-color: black;
            }
        </style>
    </head>
    <body>
        <p> 
            這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
        </p>
        <div id="test2"></div>
        <script type="text/javascript">
            window.onload = function() {
                const el = document.getElementById('test2');
                let left = 0;
        const startTimeStamp = Date.now();
                const fn = function() {
                    left += 2;
                    if(Date.now() - startTimeStamp > 2000) {
            return;
          }
                    el.style.left = left + 'px';
                    return window.requestAnimationFrame(fn);
                }
                window.requestAnimationFrame(fn)
            }
        </script>
    </body>
</html>

選取動畫過程當中的一幀,瀏覽器的處理過程以下:
圖片描述

能夠看到,在這裏幀裏,瀏覽器走完了完整的像素管道:JavaScript ->Style->Layout->Paint->Composite。

純CSS動畫

咱們用純css來實現動畫:

<html>
    <head>
        <style type="text/css">
            #test2 {
                margin-top: 100px;
                width: 100px;
                height: 100px;
                position: relative;
                background-color: black;
                animation: move 2s;
                animation-fill-mode: forwards;
            }
            @keyframes move {
                0% {
                    transform: translate(0);
                }
                100% {
                    transform: translate(200px);
                }
            }
        </style>
    </head>
    <body>
        <p> 
        這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
        </p>
        <div id="test2"></div>
    </body>
</html>

咱們來看看動畫進行過程當中:

圖片描述

能夠看到,主線程裏沒有任務在執行,而Composite線程、Raster線程以及GPU在工做。

JS與CSS3相結合

<html>
    <head>
        <style type="text/css">
            #test2 {
                margin-top: 100px;
                width: 100px;
                height: 100px;
                position: relative;
                background-color: black;
            }
        </style>
    </head>
    <body>
        <p> 
            這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字,這是一段無用的文字
        </p>
        <div id="test2"></div>
        <script type="text/javascript">
            window.onload = function() {
                const el = document.getElementById('test2');
                let left = 0;
            const startTimeStamp = Date.now();
                const fn = function() {
                    left += 2;
                    if(Date.now() - startTimeStamp > 2000) {
                return;
            }
                    el.style.transform = `translate(${left}px)`;
                    return window.requestAnimationFrame(fn);
                }
                window.requestAnimationFrame(fn);
            }
        </script>
    </body>
</html>

動畫運行時,瀏覽器的處理過程以下圖所示,並無觸發Layout和paint。

圖片描述

小結

從上面的幾個實例能夠看到,在僅使用css動畫時,動畫過程徹底交由Composite線程處理,釋放了主線程。事實上,在執行純css3動畫時,瀏覽器會將響應的元素提高到一個單獨的合成層,不會影響到頁面上的其餘元素。

使用js操做css3屬性,也能夠跳過Layout和Paint。

固然,並非全部的css屬性均可以跳過Layout和Paint僅觸發Composite,常見的屬性是:transformopacity。具體屬性能夠到下面的網址查看:

https://csstriggers.com/

這裏還有幾點補充的地方:

  1. 動畫開始時,都會觸發一次paint。
  2. 對於純css3操做transform和opacity的動畫,在動畫開始時,瀏覽器會自動將動畫元素提高爲合成層,可是在動畫結束後,合成層會失效。在動畫結束後(合成層失效)的那一幀,瀏覽器是會觸發Paint的。若是咱們強制將動畫元素提高爲合成層,動畫結束後的那一幀,就不會觸發Paint了。
  3. 對於js操做css3的transform和opacity的動畫,在動畫過程當中,瀏覽器不會自動將動畫元素提高爲合成層,可是也不會觸發Paint。在動畫結束的那一幀,無論咱們是否強制將動畫元素提高爲合成層,當頁面動畫元素嵌套複雜時,可能會觸發Paint。

總結

想要實現高性能的動畫,儘可能使用css動畫或者使用js操做css3屬性的方式,同時,要注意動畫用到的css3屬性。動畫的目標就是跳過瀏覽器的Layout和Paint,僅觸發Composite。

對於特定的動畫元素,咱們能夠適當將其提高到合成層,這樣該元素不會影響到頁面其餘地方。固然,合成層的使用要適當,由於合成層會帶來內存壓力。

寫在後面

本文從瀏覽器渲染原理入手,談到了如何實現一個高效的動畫。在寫做本文的過程當中,學習、鞏固了不少的知識。還有一些更深刻的點值得去繼續研究。符合預期。

參考資料:


圖片描述

相關文章
相關標籤/搜索