reflow和repaint引發的效能問題

是熊大啊發表於2018-02-26

reflow和repaint在pc端只要不是懷有明知山有虎,偏向虎山行的心態寫程式碼,這兩貨幾乎不會引發效能問題, 但是移動端的渲染能力和pc端差了不止一個大截,一個不小心reflow和repaint就成了移動端的“效能殺手”。所以瞭解reflow和repaint也是很有必要的,在考量頁面效能的時候分析reflow和repaint也算是一個切入點。

是什麼


reflow 迴流,或者叫重排都可以。迴流(reflow)這個名詞指的是瀏覽器為了重新渲染部分或全部的文件而重新計算文件中元素的位置和幾何結構的過程。

簡單來說就是當頁面佈局或者幾何屬性改變時就需要reflow。

在一個頁面中至少在頁面剛載入的時候有一次reflow,在reflow的過程中瀏覽器會將render tree中受影響的節點失效,再重新構建render tree,有時候,即使僅僅迴流一個單一的元素,也可能要求它的父元素以及任何跟隨它的元素也產生迴流

repaint重繪,當頁面中的元素只需要更新樣式風格不影響佈局,比如更換背景色background-color,這個過程就是重繪。

如何觸發


reflow

從reflow的定義中就可以聽出一些來,元素的佈局和幾何屬性改變時就會觸發reflow。主要有這些屬性:

  • 盒模型相關的屬性: width,height,margin,display,border,etc

  • 定位屬性及浮動相關的屬性: top,position,float,etc

  • 改變節點內部文字結構也會觸發迴流: text-align, overflow, font-size, line-height, vertival-align,etc

除開這三大類的屬性變動會觸發reflow,以下情況也會觸發:

  • 調整視窗大小
  • 樣式表變動
  • 元素內容變化,尤其是輸入控制元件
  • dom操作
  • css偽類啟用
  • 計算元素的offsetWidth、offsetHeight、clientWidth、clientHeight、width、height、scrollTop、scrollHeight

repaint

頁面中的元素更新樣式風格相關的屬性時就會觸發重繪,如background,color,cursor,visibility,etc

注意:由頁面的渲染過程可知,reflow必將會引起repaint,而repaint不一定會引起reflow

瞭解有哪些屬性值改變會觸發迴流或者重繪點選這裡

聰明的瀏覽器


設想一個這樣的場景,我們需要在一個迴圈中不斷修改一個dom節點的位置或者是內容

   document.addEventListener('DOMContentLoaded', function () {
    var date = new Date();
    for (var i = 0; i < 70000; i++) {
        var tmpNode = document.createElement("div");
        tmpNode.innerHTML = "test" + i;
        document.body.appendChild(tmpNode);
    }
    console.log(new Date() - date);
}); 
複製程式碼

這裡多次測量消耗時間大概在500ms(執行環境均為pc端,小霸王筆記本)。看到這個結果可能就有疑問了,這裡有70000次內容的修改,就有70000reflow操作,也就用了500ms的時間(歸功於遲緩的dom操作),說好的reflow消耗效能呢。

reflow和repaint引發的效能問題
其實在這個過程中,瀏覽器為了防止我們犯二把多次reflow操作放在迴圈中而引發瀏覽器假死,做了一個聰明的小動作。它會收集reflow操作到快取佇列中直到一定的規模或者過了特定的時間,再一次性地flush佇列,反饋到render tree中,這樣就將多次的reflow操作減少為少量的reflow。但是這樣的小動作帶來了另外一個問題,如果我們想要在一次reflow過後就獲取元素變動過後的值呢?這個時候瀏覽器為了獲取真實的值就不得不立即flush快取的佇列。這些值或方法包括:

  • offsetTop/Left/Width/Height
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • getComputedStyle(), or currentStyle in IE

犯二程式碼如下:

        document.addEventListener('DOMContentLoaded', function () {
            var date = new Date();
            for (var i = 0; i < 70000; i++) {
                var tmpNode = document.createElement("div");
                tmpNode.innerHTML = "test" + i;
                document.body.offsetHeight; // 獲取body的真實值
                document.body.appendChild(tmpNode);
            }
            console.log("speed time", new Date() - date);
        });
複製程式碼

一般人應該不會去執行這種程式碼,如果你執行了的話,恭喜你的電腦-1s。但是如果沒有衡量指標,優化效能也就無從談起。

“If you cannot measure it, you cannot improve it.” -Lord Kelvin

為了防止瀏覽器假死,把迴圈次數都改為7000次,得出的結果是(多次平均):

  • 獲取了真實值的樣例用時約18000ms
  • 沒有獲取真實值的樣例用時約50ms

通過這兩個樣例印證了瀏覽器確實有優化reflow的小動作,聰明的程式設計師不會依賴瀏覽器的優化策略,在日常開發中遇到for迴圈就應該慎重編寫迴圈體內部的程式碼。

減少reflow和repaint


如何減少reflow和repaint呢?回到定義去,reflow在頁面佈局或者定位發生變化時才會發生,從定義中我們至少可以得出兩個優化思路

  • 減少reflow操作
  • 替代會觸發迴流的屬性

減少reflow操作

其本質上為減少對render tree的操作。render tree也就是渲染樹,它的每個節點都是可見,且包含該節點的內容和對應的規則樣式,這也是render tree和dom數最大的區別所在, 減少reflow操作,主旨是合併多個reflow,最後再反饋到render tree中,諸如:

1,直接更改classname

    // 不好的寫法
    var left = 1;
    var top = 2;
    ele.style.left = left + "px";
    ele.style.top = top + "px";
    // 比較好的寫法
    ele.className += " className1";
複製程式碼

或者直接修改cssText:

    ele.style.cssText += ";
    left: " + left + "px;
    top: " + top + "px;";
複製程式碼

2.讓頻繁reflow的元素“離線”

  • 使用DocumentFragment進行快取操作,引發一次迴流和重繪;
  • 使用display:none,只引發兩次迴流和重繪;
  • 使用cloneNode(true or false) 和 replaceChild 技術,引發一次迴流和重繪;

Dom規定文件片段(document fragment)是一種“輕量級”的文件,可以包含和控制節點,但不會想完整的文件那樣佔用額外的資源。雖然不能把文件片段直接新增到文件中,但是可以將它作為一個“倉庫”來使用,即可以在裡面儲存將來可能會新增到文件中的節點。 比如最開始的樣例結合DocumentFragment就可以這樣寫:

    document.addEventListener('DOMContentLoaded', function () {
        var date = new Date(),
            fragment = document.createDocumentFragment();
        for (var i = 0; i < 7000; i++) {
            var tmpNode = document.createElement("div");
            tmpNode.innerHTML = "test" + i;
            fragment.appendChild(tmpNode);
        }
        document.body.appendChild(fragment);
        console.log("speed time", new Date() - date);
    });
複製程式碼

將多個修改結果收納到了documentFragment這個“倉庫”中,這個過程並不會影響到render tree,待迴圈完畢再將這個“倉庫”的“存貨”新增到dom上,以此達到減少reflow的目的,使用cloneNode也是同理。 而使用display:none來降低reflow的效能開銷的原理在於使節點從render tree中失效,等經過多個會觸發reflow操作後再“上線”

3.減少會flush快取佇列屬性的訪問次數,如果一定要訪問,使用快取

// 不好的寫法
for(let i = 0; i < 20; i++ ) { 
    el.style.left = el.offsetLeft + 5 + "px"; 
    el.style.top = el.offsetTop + 5 + "px"; 
}
// 比較好的寫法
var left = el.offsetLeft, 
top = el.offsetTop, 
s = el.style; 
for (let i = 0; i < 20; i++ ) { 
    left += 5;
    top += 5; 
    s.left = left + "px"; 
    s.top = top + "px"; 
}
複製程式碼

替代會觸發reflow和repaint的屬性

我們可以將一些會觸發迴流的屬性替換,來避免reflow。比如用translate代替top,用opacity替代visibility

樣例程式碼:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #react {
            position: relative;
            top: 0;
            width: 100px;
            height: 100px;
            background-color: red;
        }
    </style>
</head>

<body>
    <div id="react"></div>
    <script type="text/javascript">
        setTimeout(() => {
            document.getElementById("react").style.top = "100px"
        }, 2000);
    </script>
</body>
</html>
複製程式碼

程式碼很簡單,頁面上有一個紅色的方塊,2秒後它的top值將會變為“100px”,為了方便體現替代的屬性可以避免reflow這裡我們使用chrome的開發者工具,部分截圖如下

reflow和repaint引發的效能問題
如上圖,在top值變為“100px”的過程中有上圖五個階段.

  • Recalculate Style,瀏覽器計算改變過後的樣式
  • Layout,這個過程就是我們說得reflow迴流過程
  • Update Layer Tree,更新Layer Tree
  • Paint,圖層的繪製過程
  • Composite Layers,合併多個圖層

我們把這五個過程用時記下:80 + Layout(73) + 72 + 20 + 69 = 316us

再用translate替代top:

-       position: relative;
-       top: 0;
+       transform: translateY(0);

-       document.getElementById("react").style.top = "100px"
+       document.getElementById("react").style.transform = "translateY(100px)"
複製程式碼

Performace截圖:

reflow和repaint引發的效能問題
可以看到用translate替換top後減少了原來的Layout也就是reflow的過程,用時:81 + 80 + 36 + 83 = 280us。 結果非常明顯315us減少到了280us。有人說這個效果不明顯呀,但是讓我們設想這樣一個業務場景,有許多網站都會有不停移動的飄窗,這種飄窗通常是用定時器實現,每隔100ms就去修改一次它的top,如果用translate的話1s就可以減少10次reflow,如果這個飄窗樣式比較多,比較複雜,那麼1秒鐘減少的10次reflow就有可能減少幾百毫秒甚至幾秒Layout的過程

我們再用opacity去替代visibility試試看。

-            document.getElementById("react").style.transform = "translateY(100px)"            
+            document.getElementById("react").style.visibility = "hidden"            
複製程式碼

Performace截圖:

reflow和repaint引發的效能問題
visibility屬性值改變只會觸發repaint,不會觸發reflow,所以只有四個階段,其中第三個階段Paint就是重繪的體現,用時:48 + 50 + Paint(14) + 71 = 183us。我們再用opacity替代visibility

+            opacity: 1;

-            document.getElementById("react").style.visibility = "hidden"    
+            document.getElementById("react").style.opacity = "0"
複製程式碼

按照上面的樣例,應該得出用opacity替代visibility後重繪也就是Paint這個過程會消失從而達到效能提升的目的,既然這樣我們來看Performace截圖:

reflow和repaint引發的效能問題
對,你沒有看錯,我也沒有截錯圖,這次不光是Paint過程沒有消失,就連Layout都出現了,驚不驚喜!意不意外!
reflow和repaint引發的效能問題
我們再來重定義一下repaint重繪,它是重新繪製當前圖層的內容,(什麼是圖層,點選檢視這篇文章)

其實opacity變化並不能改變這個圖層的內容,改變的只是當前圖層的alpha通道的值,從而來決定這個圖層顯不顯示。但是這opacity變化的元素並不是單獨的圖層,而是在document這個圖層上的,如下Layers截圖:

reflow和repaint引發的效能問題

就是說瀏覽器並不能因為圖層裡面有一個opacity為0的元素就讓整個圖層的alpha通道變為零,而讓整個圖層不顯示,所以就有了Layout和Paint這兩個過程。解決辦法也很簡單那就是直接讓這個元素單獨為一個圖層

修改css新建圖層有兩種辦法:

  • will-change:transform
  • transform:translateZ(0)

這裡我們用下面一個

+   transform: translateZ(0);
複製程式碼

Performace截圖:

reflow和repaint引發的效能問題
現在就和理想中的情況一樣了,用opacity替代visibility可以避免Paint重繪的過程。再來看看用時: 66 + 53 + 52 = 171us

這裡由於我變動的元素非常簡單,只有一個簡單的div,減少Paint過程帶來的優化收益並不是很明顯,如果是Paint過程是毫秒級別減少Paint過程的效果還是可觀的。

由上述兩個替代會觸發reflow和repaint的屬性取得效能優化收益的例子中可以看出,這個方法是可行的,除開第一點減少reflow操作和第二點替換屬性以外還有一些方法可以減少reflow和repaint

  • 減少table的使用

  • 動畫實現的速度選擇

  • 對於動畫新建圖層

    table自帶的樣式和一些非常方便的特性會方便我們的開發,但是table也有一些與生俱來的效能缺陷,如果想要修改表格裡不管哪一個單元格,都會導致整張表格的重新Layout,如果這個表格很大,效能的消耗會有一個上升成本的。

圖層的運用


在上一個樣例中我們新建了一個圖層實現了opacity替代visibility去減少repaint的可行性,那麼圖層還有什麼其他運用嗎?答案是有的,我們可以將一些頻繁重繪迴流的DOM元素作為一個圖層,那麼這個DOM元素的重繪和迴流的影響只會在這個圖層中,當然如果你為每一個元素都建立一個圖層那樣肯定也會聰明反被聰明誤,還記得上述的Performance截圖中的過程嗎,最後一個Composite Layers這個過程就是合併多個圖層的,圖層過多這個過程會非常耗時,其實這個過程本身也非常耗時,原則上是在必要的情況下才會新建圖層來減少重繪和迴流的影響範圍,到底使不使用就需要開發人員在業務情景中balance. 在Chrome瀏覽器下可以這樣建立圖層:

  • 3D或透視變換CSS屬性(perspective transform)
  • 使用加速視屏解碼的video標籤
  • 擁有3D(WebGL)上下文或加速的2D上下文的canvas
  • 混合外掛如(如Flash)
  • 對自己的opacity做CSS動畫或使用一個動畫webkit變換的元素
  • 擁有加速CSS過濾器的元素(GPU加速)
  • 元素有一個包含複合層的後代節點
  • 元素有一個z-index較低且包含一個複合層的兄弟元素
  • will-change: transform;

大體思路就是我們把頻繁重繪迴流的DOM元素作為一個圖層,那麼這個DOM元素的重繪和迴流的影響只會在這個圖層中,來提升效能。舉個例子,我們開啟chrome開發者工具中的Layers,然後開啟某網站

reflow和repaint引發的效能問題
從紅框中可以看出這個網站已經被分為了很多圖層,當前選中的的這個baner圖層在檢視區域已經標註出來,由圖可知,將一個經常觸發迴流和重繪的元素新開圖層也算一個優化效能的做法。我們再勾選這個選項

reflow和repaint引發的效能問題
瀏覽器會用綠色高亮出當前正在repaint的元素,勾選上過後我們開啟一個視訊:

reflow和repaint引發的效能問題
可以看到視訊在播放過程中一直處於高亮狀態,這個不難理解,video為單獨一個圖層,在整個視訊播放過程中video接受到傳送過來的每一幀,都會將觸發video所在圖層的重繪。

結語

簡單回顧一下本文,我們最開始聊了一下reflow和repaint是什麼,如何觸發它們,接下來談了一下瀏覽器在處理它們所採取的策略,最後就是如何避免reflow和repaint帶來的效能開銷,還補充了一下圖層的存在意義和簡單運用。 其實在優化reflow和repaint上就是兩點:

  • 避免使用觸發reflow、repaint的css屬性
  • 將reflow、repaint的影響範圍限制在單獨的圖層之內

參考資料

https://csstriggers.com

http://blog.csdn.net/luoshengyang/article/details/50661553

相關文章