減少迴流重繪的方法及相關的性能測試(performance)結果分析

迴流必將引起重繪,而重繪不一定引起迴流
我們經常會看到上面這句話,那麼迴流和重繪是什麼呢

迴流

計算DOM節點在設備視口(viewport)內的確切位置和大小的過程,會在頁面節點的幾何屬性或者佈局發生變化時發生的

發生迴流的情況

添加或刪除可見的DOM元素
元素的位置發生變化
元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代。
頁面一開始渲染的時候(這肯定避免不了)
瀏覽器的窗口尺寸變化(因爲迴流是根據視口的大小來計算元素的位置和大小的)

重繪

重繪的發生,是由於節點的幾何屬性發生改變或者由於樣式發生改變但又不會影響佈局的改變引起的,通過可見節點和具體樣式信息,將渲染樹的每個節點都轉換爲屏幕上的實際像素

發生重繪的情況

發生迴流的時候,也會發生重繪,所以上面的情況同樣會觸發重繪
我的理解是,只要頁面內容發生變化,都會導致重繪的發生(如果不對請指正)

減少迴流重繪方法和性能測試

tip:在下面的性能測試中,我給出的圖是某一次測試的,但實際上每種方法我都測試了很多次,每次都達到時間消耗變少的效果

1. 對那些DOM元素的操作,不要逐一操作,而是將其一次性操作,直接使用class或者一次性寫入多個style

測試demo

<!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>
            .newClass{
                margin:10px;
                font-size: 20px;
                color:#f00;
            }
        </style>
    </head>
    <body>
        <button onclick="oneByone()">一個個修改</button>
        <button onclick="byClass()">使用class修改</button>
        <li>111</li>
        <!-- 下面還有999個li標籤 -->
        <!-- ... -->
        <script>
            let li = document.querySelectorAll("li")

            function oneByone() {
                for (let i = 0; i < li.length; i++) {
                    li[i].style.margin = "10px";
                    li[i].style.fontSize = "20px";
                    li[i].style.color = "#f00"
                }
            }

            function byClass() {
                for (let i = 0; i < li.length; i++) {
                    li[i].className = "newClass"
                }
            }
    </script>
    </body>
</html>

在這裏,我通過點擊兩個按鈕,使用performance監控,將監控圖進行比較,這裏主要會看的是Scripting和Rendering,即js執行和css解析,如圖


可以看到,通過class來修改樣式的時候,Scripting和Rendering都比一個一個修改消耗的時間少

2. 對於一些樣式的修改,儘量採用不會影響頁面佈局的修改

比如display:none和visibility: hidden,在可能的情況下儘量使用visibility
測試demo

<!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>
    </head>
    <body>
        <button onclick="vi()">vBtn</button>
        <button onclick="di()">dBtn</button>
        <li></li>
        <!-- 下面還有999個li標籤 -->
        <!-- ... -->
        <script>
            let li = document.querySelectorAll("li")

            function di() {
                for (let i = 0; i < li.length; i++) {
                    li[i].style.display = "none"
                }
            }

            function vi() {
                for (let i = 0; i < li.length; i++) {
                    li[i].style.visibility = "hidden"
                }
            }
    </script>
    </body>
</html>

這裏我同樣使用performance來監控性能,按照我們上面的理論,使用display:none會引起迴流,而使用visibility:hidden不會,所以我們重點看火焰圖中的Rendering也就是紫色的部分

顯然可以看出,按下vBtn時消耗的時間是43ms,而按下dBtn消耗的時間是153ms,區別是很明顯的

3. 對那些需要多次修改的操作,可以先使DOM元素脫離文檔流,然後對其進行多次修改,最後再將其放回到文檔流中,這樣只會引起兩次迴流

測試demo

<!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>
    </head>
    <body>
        <button onclick="inFloat()">直接修改</button>
        <button onclick="outOfFloat()">移出文檔流</button>
        <li>111</li>
        <!-- 下面還有999個li標籤 -->
        <!-- ... -->
        <script>
            let li = document.querySelectorAll("li")

            function inFloat() {
                for (let i = 0; i < li.length; i++) {
                    li[i].style.margin = "10px";
                    li[i].style.padding = "10px";
                    li[i].style.border = "1px solid";
                    li[i].style.fontSize = "20px";
                }
            }

            function outOfFloat() {
                for (let i = 0; i < li.length; i++) {
                    li[i].style.display = "none";
                    li[i].style.margin = "10px";
                    li[i].style.padding = "10px";
                    li[i].style.border = "1px solid";
                    li[i].style.fontSize = "20px";
                    li[i].style.display = "list-item";
                }
            }
    </script>
    </body>
</html>


如圖,我們可以看到,脫離文檔流後,Rendering的時間變少了,符合我們上面的理論,但是,可以看到Scripting那裏,脫離文檔流的時候,js執行的時間變長了,當然,這是因爲我們爲了讓元素脫離文檔流執行的操作導致的,我們可以進一步查看scripting的內容



如圖可以直觀地看到,outOfFloat方法的消耗比inFloat方法的消耗多,所以採取這種方法來減少迴流和重繪性能消耗的時候,要考慮到迴流和重繪減少的時間,是否會多於增加的js語句執行帶來的消耗,這樣才能真正地做到整體的性能優化

4. 將會導致迴流重繪的修改放在文檔流比較後的位置

我們修改頁面的元素後,如果文檔流前面部分的佈局修改了,可能會導致文檔流後面的內容也要進行迴流重繪,而如果這部分修改是放在後面的,那回流和重繪的消耗就會比較小了

測試demo

<!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>
    </head>
    <body>
        <button onclick="front()">修改前面一半</button>
        <button onclick="behind()">修改後面一半</button>
        <li>111</li>
        <!-- 下面還有999個li標籤 -->
        <!-- ... -->
        <script>
            let li = document.querySelectorAll("li")

            function front() {
                for (let i = 0; i < li.length / 2; i++) {
                    li[i].style.margin = "10px";
                    li[i].style.padding = "10px";
                    li[i].style.border = "1px solid";
                    li[i].style.fontSize = "20px";
                }
            }

            function behind() {
                for (let i = li.length / 2; i < li.length; i++) {
                    li[i].style.margin = "10px";
                    li[i].style.padding = "10px";
                    li[i].style.border = "1px solid";
                    li[i].style.fontSize = "20px";
                }
            }
    </script>
    </body>
</html>

結果如圖所示,將會導致迴流和重繪的元素放在文檔流後面,在css解析部分還是會消耗比較少的時間,雖然這裏時間相差不多,但我多次測試時每次都是改變後面消耗會更少

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章