【開發必看】你真的瞭解迴流和重繪嗎?

騰訊雲加社群發表於2019-01-14

本文由雲+社群發表

迴流和重繪可以說是每一個web開發者都經常聽到的兩個詞語,可是可能有很多人不是很清楚這兩步具體做了什麼事情。最近有空對其進行了一些研究,看了一些部落格和書籍,整理了一些內容並且結合一些例子,寫了這篇文章,希望可以幫助到大家。

瀏覽器的渲染過程

本文先從瀏覽器的渲染過程來從頭到尾的講解一下回流重繪,如果大家想直接看如何減少迴流和重繪,優化效能,可以跳到後面。(這個渲染過程來自MDN

img
瀏覽器渲染過程

新增描述

從上面這個圖上,我們可以看到,瀏覽器渲染過程如下:

  1. 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
  2. 將DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
  3. Layout(迴流):根據生成的渲染樹,進行迴流(Layout),得到節點的幾何資訊(位置,大小)
  4. Painting(重繪):根據渲染樹以及迴流得到的幾何資訊,得到節點的絕對畫素
  5. Display:將畫素髮送給GPU,展示在頁面上。(這一步其實還有很多內容,比如會在GPU將多個合成層合併為同一個層,並展示在頁面中。而css3硬體加速的原理則是新建合成層,這裡我們不展開,之後有機會會寫一篇部落格)

渲染過程看起來很簡單,讓我們來具體瞭解下每一步具體做了什麼。

生成渲染樹

img
渲染樹構建

為了構建渲染樹,瀏覽器主要完成了以下工作:

  1. 從DOM樹的根節點開始遍歷每個可見節點。
  2. 對於每個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
  3. 根據每個可見節點以及其對應的樣式,組合生成渲染樹。

第一步中,既然說到了要遍歷可見的節點,那麼我們得先知道,什麼節點是不可見的。不可見的節點包括:

  • 一些不會渲染輸出的節點,比如script、meta、link等。
  • 一些通過css進行隱藏的節點。比如display:none。注意,利用visibility和opacity隱藏的節點,還是會顯示在渲染樹上的。只有display:none的節點才不會顯示在渲染樹上。

從上面的例子來講,我們可以看到span標籤的樣式有一個display:none,因此,它最終並沒有在渲染樹上。

注意:渲染樹只包含可見的節點

迴流

前面我們通過構造渲染樹,我們將可見DOM節點以及它對應的樣式結合起來,可是我們還需要計算它們在裝置視口(viewport)內的確切位置和大小,這個計算的階段就是迴流。

為了弄清每個物件在網站上的確切大小和位置,瀏覽器從渲染樹的根節點開始遍歷,我們可以以下面這個例項來表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
複製程式碼

我們可以看到,第一個div將節點的顯示尺寸設定為視口寬度的50%,第二個div將其尺寸設定為父節點的50%。而在迴流這個階段,我們就需要根據視口具體的寬度,將其轉為實際的畫素值。(如下圖)

img
迴流

重繪

最終,我們通過構造渲染樹和迴流階段,我們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何資訊(位置、大小),那麼我們就可以將渲染樹的每個節點都轉換為螢幕上的實際畫素,這個階段就叫做重繪節點。

既然知道了瀏覽器的渲染過程後,我們就來探討下,何時會發生迴流重繪。

何時發生迴流重繪

我們前面知道了,迴流這一階段主要是計算節點的位置和幾何資訊,那麼當頁面佈局和幾何資訊發生變化的時候,就需要回流。比如以下情況:

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

注意:迴流一定會觸發重繪,而重繪不一定會迴流

根據改變的範圍和程度,渲染樹中或大或小的部分需要重新計算,有些改變會觸發整個頁面的重排,比如,滾動條出現的時候或者修改了根節點。

瀏覽器的優化機制

現代的瀏覽器都是很聰明的,由於每次重排都會造成額外的計算消耗,因此大多數瀏覽器都會通過佇列化修改並批量執行來優化重排過程。瀏覽器會將修改操作放入到佇列裡,直到過了一段時間或者操作達到了一個閾值,才清空佇列。但是!當你獲取佈局資訊的操作的時候,會強制佇列重新整理,比如當你訪問以下屬性或者使用以下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect
  • 具體可以訪問這個網站:gist.github.com/paulirish/5…

以上屬性和方法都需要返回最新的佈局資訊,因此瀏覽器不得不清空佇列,觸發迴流重繪來返回正確的值。因此,我們在修改樣式的時候,**最好避免使用上面列出的屬性,他們都會重新整理渲染佇列。**如果要使用它們,最好將值快取起來。

減少迴流和重繪

好了,到了我們今天的重頭戲,前面說了這麼多背景和理論知識,接下來讓我們談談如何減少迴流和重繪。

最小化重繪和重排

由於重繪和重排可能代價比較昂貴,因此最好就是可以減少它的發生次數。為了減少發生次數,我們可以合併多次對DOM和樣式的修改,然後一次處理掉。考慮這個例子

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
複製程式碼

例子中,有三個樣式屬性被修改了,每一個都會影響元素的幾何結構,引起迴流。當然,大部分現代瀏覽器都對其做了優化,因此,只會觸發一次重排。但是如果在舊版的瀏覽器或者在上面程式碼執行的時候,有其他程式碼訪問了佈局資訊(上文中的會觸發迴流的佈局資訊),那麼就會導致三次重排。

因此,我們可以合併所有的改變然後依次處理,比如我們可以採取以下的方式:

  • 使用cssText
const el = document.getElementById('test'); 
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
複製程式碼
  • 修改CSS的class
const el = document.getElementById('test');
el.className += ' active';
複製程式碼

批量修改DOM

當我們需要對DOM對一系列修改的時候,可以通過以下步驟減少迴流重繪次數:

  1. 使元素脫離文件流
  2. 對其進行多次修改
  3. 將元素帶回到文件中。

該過程的第一步和第三步可能會引起迴流,但是經過第一步之後,對DOM的所有修改都不會引起迴流重繪,因為它已經不在渲染樹了。

有三種方式可以讓DOM脫離文件流:

  • 隱藏元素,應用修改,重新顯示
  • 使用文件片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文件。
  • 將原始元素拷貝到一個脫離文件的節點中,修改節點後,再替換原始的元素。

考慮我們要執行一段批量插入節點的程式碼:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);
複製程式碼

如果我們直接這樣執行的話,由於每次迴圈都會插入一個新的節點,會導致瀏覽器迴流一次。

我們可以使用這三種方式進行優化:

隱藏元素,應用修改,重新顯示

這個會在展示和隱藏節點的時候,產生兩次迴流

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
複製程式碼

使用文件片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文件

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
複製程式碼

將原始元素拷貝到一個脫離文件的節點中,修改節點後,再替換原始的元素。

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
複製程式碼

對於上面這三種情況,我寫了一個demo在safari和chrome上測試修改前和修改後的效能。然而實驗結果不是很理想。

原因:原因其實上面也說過了,現代瀏覽器會使用佇列來儲存多次修改,進行優化,所以對這個優化方案,我們其實不用優先考慮。

避免觸發同步佈局事件

上文我們說過,當我們訪問元素的一些屬性的時候,會導致瀏覽器強制清空佇列,進行強制同步佈局。舉個例子,比如說我們想將一個p標籤陣列的寬度賦值為一個元素的寬度,我們可能寫出這樣的程式碼:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}
複製程式碼

這段程式碼看上去是沒有什麼問題,可是其實會造成很大的效能問題。在每次迴圈的時候,都讀取了box的一個offsetWidth屬性值,然後利用它來更新p標籤的width屬性。這就導致了每一次迴圈的時候,瀏覽器都必須先使上一次迴圈中的樣式更新操作生效,才能響應本次迴圈的樣式讀取操作。每一次迴圈都會強制瀏覽器重新整理佇列。我們可以優化為:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}
複製程式碼

同樣,我也寫了個demo來比較兩者的效能差異。你可以自己點開這個demo體驗下。這個對比的效能差距就比較明顯。

對於複雜動畫效果,使用絕對定位讓其脫離文件流

對於複雜動畫效果,由於會經常的引起迴流重繪,因此,我們可以使用絕對定位,讓它脫離文件流。否則會引起父元素以及後續元素頻繁的迴流。這個我們就直接上個例子

開啟這個例子後,我們可以開啟控制檯,控制檯上會輸出當前的幀數(雖然不準)。

img

新增描述

從上圖中,我們可以看到,幀數一直都沒到60。這個時候,只要我們點選一下那個按鈕,把這個元素設定為絕對定位,幀數就可以穩定60。

css3硬體加速(GPU加速)

比起考慮如何減少迴流重繪,我們更期望的是,根本不要回流重繪。這個時候,css3硬體加速就閃亮登場啦!!

劃重點:

1. 使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪 。

2. 對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能。

本篇文章只討論如何使用,暫不考慮其原理,之後有空會另外開篇文章說明。

如何使用

常見的觸發硬體加速的css屬性:

  • transform
  • opacity
  • filters
  • Will-change

效果

我們可以先看個例子。我通過使用chrome的Performance捕獲了動畫一段時間裡的迴流重繪情況,實際結果如下圖:

img

新增描述

從圖中我們可以看出,在動畫進行的時候,沒有發生任何的迴流重繪。如果感興趣你也可以自己做下實驗。

重點

  • 使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪
  • 對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能。

css3硬體加速的坑

當然,任何美好的東西都是會有對應的代價的,過猶不及。css3硬體加速還是有坑的:

  • 如果你為太多元素使用css3硬體加速,會導致記憶體佔用較大,會有效能問題。
  • 在GPU渲染字型會導致抗鋸齒無效。這是因為GPU和CPU的演算法不同。因此如果你不在動畫結束的時候關閉硬體加速,會產生字型模糊。

總結

本文主要講了瀏覽器的渲染過程、瀏覽器的優化機制以及如何減少甚至避免迴流和重繪,希望可以幫助大家更好的理解迴流重繪。

參考文獻

此文已由作者授權騰訊雲+社群在各渠道釋出

獲取更多新鮮技術乾貨,可以關注我們騰訊雲技術社群-雲加社群官方號及知乎機構號

相關文章