reflow和repaint引起的性能問題

reflow和repaint在pc端只要不是懷有明知山有虎,偏向虎山行的心態寫代碼,這兩貨幾乎不會引起性能問題, 可是移動端的渲染能力和pc端差了不止一個大截,一個不當心reflow和repaint就成了移動端的「性能殺手」。因此瞭解reflow和repaint也是頗有必要的,在考量頁面性能的時候分析reflow和repaint也算是一個切入點。javascript

是什麼


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

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

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

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

如何觸發


reflow

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

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

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

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

除開這三大類的屬性變更會觸發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操做放在循環中而引起瀏覽器假死,作了一個聰明的小動做。它會收集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的開發者工具,部分截圖以下

如上圖,在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截圖:

能夠看到用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截圖:

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截圖:

對,你沒有看錯,我也沒有截錯圖,此次不光是Paint過程沒有消失,就連Layout都出現了,驚不驚喜!意不意外!
咱們再來重定義一下repaint重繪,它是從新繪製當前圖層的內容,(什麼是圖層, 點擊查看這篇文章)

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

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

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

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

這裏咱們用下面一個

+   transform: translateZ(0);
複製代碼

Performace截圖:

如今就和理想中的狀況同樣了,用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,而後打開某網站

從紅框中能夠看出這個網站已經被分爲了不少圖層,當前選中的的這個baner圖層在視圖區域已經標註出來,由圖可知,將一個常常觸發迴流和重繪的元素新開圖層也算一個優化性能的作法。咱們再勾選這個選項

瀏覽器會用綠色高亮出當前正在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

相關文章
相關標籤/搜索