迴流、重繪及其優化
渲染過程
渲染引擎通過通過網路請求接收渲染內容
- 解析HTML抽象DOM tree
- 抽象出Render tree
- 佈局(layout)render tree
- 繪畫render tree
抽象DOM tree
渲染引擎的第一步是解析html文件並將解析的元素轉換為dom樹中的實際dom節點。
抽象CSSOM tree
當瀏覽器解析dom的時候,遇到link標籤,引用外部的css樣式表,引擎會將css抽象成cssom
構建渲染樹
HTML中的可視指令與來自cssom樹的樣式資料結合使用來建立渲染樹。
為了構建渲染樹,瀏覽器大致如下:
- 從dom樹的根開始,遍歷每個可見節點。某些節點不可見(例如,指令碼標記,元標記等),並且由於它們未反映在呈現的輸出中而被省略。display:none 也會使節點省略
- 對於每個可見節點,瀏覽器找到適當的匹配cssom規則並應用它們
- 它會發布帶有內容和計算樣式的可見節點
- 每個渲染器代表一個矩形區域,通常對應於一個節點的CSS框。
它包括幾何資訊,如寬度,高度和位置
渲染樹的佈局
當渲染器被建立並新增到樹中時,它沒有位置和大小。計算這些值稱為佈局。
html使用基於流的佈局模型,這意味著大多數時候它可以一次性計算幾何。座標系相對於根渲染器。使用頂部和左側座標。
佈局是一個遞迴過程,從根元素開始,也就是html,每個渲染器都會去計算他自己的位置和大小
繪製渲染樹
在這個階段,遍歷渲染器樹,呼叫渲染器的paint()方法在螢幕上顯示內容。
渲染分為全域性渲染和增量渲染
處理指令碼和樣式表的順序
當解析器到達script標記時,指令碼將被立即解析並執行。
文件的解析將暫停,直到指令碼執行完畢。
這意味著該過程是同步的
這也是為什麼把script標籤放在body結束之前
html5新增了一個選項,將指令碼標記為非同步,以便它可以被其他執行緒解析和執行。
迴流和重繪(reflow和repaint)
- 迴流: 意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹;
- 重繪:意味著元素髮生的改變隻影響了節點的一些樣式(背景色,邊框顏色,文字顏色等),只需要應用新樣式繪製這個元素就可以了;
何時觸發迴流和重繪
-
repaint重繪:
- reflow迴流必定引起repaint重繪,重繪可以單獨觸發
- 背景色、顏色、字型改變(注意:字型大小發生變化時,會觸發迴流)
-
reflow迴流:
- 頁面第一次渲染(初始化)
- DOM樹變化(如:增刪節點)
- Render樹變化(如:padding改變)
- 瀏覽器視窗resize
- 當你查詢佈局資訊,包括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
- 避免使用setTimeout setInterval 來更新檢視,這會在render之後提交修改需求
- 在micro-tasks中修改dom。這會在render之前提交修改需求
- 把script標籤放在body結束之前,或者使用非同步script(defer, async)
- 把計算量大的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
- 使元素脫離文件流
- 對其應用多重修改
- 把元素帶回文件中
這個過程會觸發兩次迴流,第一步和第三步。把會觸發多次迴流的步驟放在第二步
三種基本方法:
- 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();
}
使元素進行動畫效果的時候脫離文件流
在元素髮生動畫效果的時候,會引起底部元素的迴流,這個影響可能很大,也可能很小,取決於元素在文件流的位置
- 動畫元素使用絕對定位,使其脫離文件流
- 這裡再進行旋轉,跳躍,都不會影響到整個頁面的迴流
- 在動畫結束時恢復定位,從而只會下移一次文件的其他元素。
參考DOM操作成本到底高在哪兒
參考高效能javascript
參考How JavaScript works: the rendering engine and tips to optimize its performance