迴流、重繪及其優化

lujs發表於2019-02-16

迴流、重繪及其優化

渲染過程

渲染引擎通過通過網路請求接收渲染內容

  1. 解析HTML抽象DOM tree
  2. 抽象出Render tree
  3. 佈局(layout)render tree
  4. 繪畫render tree

抽象DOM tree

渲染引擎的第一步是解析html文件並將解析的元素轉換為dom樹中的實際dom節點。
image

抽象CSSOM tree

當瀏覽器解析dom的時候,遇到link標籤,引用外部的css樣式表,引擎會將css抽象成cssom
image

構建渲染樹

HTML中的可視指令與來自cssom樹的樣式資料結合使用來建立渲染樹。
image
為了構建渲染樹,瀏覽器大致如下:

  • 從dom樹的根開始,遍歷每個可見節點。某些節點不可見(例如,指令碼標記,元標記等),並且由於它們未反映在呈現的輸出中而被省略。display:none 也會使節點省略
  • 對於每個可見節點,瀏覽器找到適當的匹配cssom規則並應用它們
  • 它會發布帶有內容和計算樣式的可見節點
  • 每個渲染器代表一個矩形區域,通常對應於一個節點的CSS框。

它包括幾何資訊,如寬度,高度和位置

渲染樹的佈局

當渲染器被建立並新增到樹中時,它沒有位置和大小。計算這些值稱為佈局。

html使用基於流的佈局模型,這意味著大多數時候它可以一次性計算幾何。座標系相對於根渲染器。使用頂部和左側座標。

佈局是一個遞迴過程,從根元素開始,也就是html,每個渲染器都會去計算他自己的位置和大小

繪製渲染樹

在這個階段,遍歷渲染器樹,呼叫渲染器的paint()方法在螢幕上顯示內容。

渲染分為全域性渲染和增量渲染

處理指令碼和樣式表的順序

當解析器到達script標記時,指令碼將被立即解析並執行。
文件的解析將暫停,直到指令碼執行完畢。
這意味著該過程是同步的

這也是為什麼把script標籤放在body結束之前

html5新增了一個選項,將指令碼標記為非同步,以便它可以被其他執行緒解析和執行。

迴流和重繪(reflow和repaint)

  • 迴流: 意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹;
  • 重繪:意味著元素髮生的改變隻影響了節點的一些樣式(背景色,邊框顏色,文字顏色等),只需要應用新樣式繪製這個元素就可以了;

何時觸發迴流和重繪

  • repaint重繪:

    1. reflow迴流必定引起repaint重繪,重繪可以單獨觸發
    2. 背景色、顏色、字型改變(注意:字型大小發生變化時,會觸發迴流)
  • reflow迴流:

    1. 頁面第一次渲染(初始化)
    2. DOM樹變化(如:增刪節點)
    3. Render樹變化(如:padding改變)
    4. 瀏覽器視窗resize
    5. 當你查詢佈局資訊,包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、呼叫了getComputedStyle()或者IE的currentStyle時,瀏覽器為了返回最新值,會觸發迴流。

優化渲染效能,減少迴流和重繪

減少reflow和repaint

  • 儘量避免改變佈局屬性。如width, height, left, top。
  • 除了transforms 或者 opacity屬性都會引起重繪,做動畫的時候要注意,儘量使用這兩個屬性;
  • 使用Flexbox。
  • 避免多次讀取部分佈局屬性(同上)
  • 將複雜的節點元素脫離文件流,降低迴流成本

javascript

  1. 避免使用setTimeout setInterval 來更新檢視,這會在render之後提交修改需求
  2. 在micro-tasks中修改dom。這會在render之前提交修改需求
  3. 把script標籤放在body結束之前,或者使用非同步script(defer, async)
  4. 把計算量大的js放在workers執行,例如解析一個大的json檔案

CSS

  • 減少選擇器的複雜性。
  • 避免逐個修改節點樣式,儘量一次性修改,減少style修改所影響元素的數量,使用cssText來替代要多次修改的style屬性
// 設定單個屬性
elt.style.color = "blue"; 

// 在單個語句中設定多個樣式
elt.style.cssText = "color: blue; border: 1px solid black"; 

// 在單個語句中設定多個樣式
elt.setAttribute("style", "color:red; border: 1px solid blue;");
  • 通過改變類名來修改樣式

DOM

  1. 使元素脫離文件流
  2. 對其應用多重修改
  3. 把元素帶回文件中

這個過程會觸發兩次迴流,第一步和第三步。把會觸發多次迴流的步驟放在第二步

三種基本方法:

  • display:none,然後修改樣式,然後在恢復
  • 使用文件片段(document fragment)在當前dom樹之外構建一個子樹,再把他拷貝迴文件。
var fragment = document.createDocumentFragment()
... 在這裡進行dom操作,可以減少迴流和重繪的次數
document.getElementById(`#app`).appendChild(fragment)
  • 將原始元素拷貝到一個脫離文件的節點中,修改副本,完成後替換原始元素
var old = document.getElementById(`#app`)
var clone = old.cloneNode(true)
... 在這裡進行dom操作,可以減少迴流和重繪的次數
old.parentNode.replaceChild(clone, old)

快取佈局資訊

前面提到在查詢佈局資訊(offsetLeft…)的時候也會引起迴流,我們在使用的時候可以把佈局資訊快取起來,減少迴流次數

這裡貼上<<高效能javascript>>中的例子:把myElement元素沿對角線移動,每次移動一個畫素,從100100的位置開始,到500500的位置結束。在timeout迴圈體中你可以使用下面的方法

// 低效的
myElement.style.left = 1 + myElement.offsetLeft + `px`
myElement.style.top = 1 + myElement.offsetTop + `px`
if (myElement.offsetTop >= 500) {
  stopAnimation();
}
// 優化
// 在迴圈外層獲取初始值
var current = myElement.offsetLeft
.
.
.
// 直接使用current變數,不再查詢偏移量
current++
myElement.style.left = current + `px`
myElement.style.top = current + `px`
if (current >= 500) {
  stopAnimation();
}

使元素進行動畫效果的時候脫離文件流
在元素髮生動畫效果的時候,會引起底部元素的迴流,這個影響可能很大,也可能很小,取決於元素在文件流的位置

  1. 動畫元素使用絕對定位,使其脫離文件流
  2. 這裡再進行旋轉,跳躍,都不會影響到整個頁面的迴流
  3. 在動畫結束時恢復定位,從而只會下移一次文件的其他元素。

參考DOM操作成本到底高在哪兒
參考高效能javascript
參考How JavaScript works: the rendering engine and tips to optimize its performance

相關文章