你應該要知道的重繪與重排

stone_發表於2019-03-02

前言

現代web框架大多都是資料驅動類的,比如 react, vue,所以開發者不需要直接接觸 DOM,修改 data 便可以驅動介面更新。但是作為前端工程師,瞭解瀏覽器的重繪與重排還是很有必要的,可以幫助我們寫出更好效能的 web 應用。

瀏覽器的渲染

  • CSS Tree: 瀏覽器將 CSS 解析成 CSSOM 的樹形結構
  • DOM Tree:瀏覽器將 HTML 解析成樹形的資料結構
  • Render Tree:將 DOM 與 CSSOM 合併成一個渲染樹

有了渲染樹(Render Tree),瀏覽器就知道網頁中有哪些節點,以及各個節點與 CSS 的關係,從而知道每個節點的位置和幾何屬性,然後繪製頁面。

重繪與重排

當 DOM 的變化影響了元素的幾何屬性(比如 width 和 height ),就會導致瀏覽器重新計算元素的幾何屬性,同樣受到該元素影響的其他元素也會發生重新計算。此時,瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程被稱為重排(也叫“迴流”)(reflow),完成重排之後,瀏覽器會重新繪製受影響的部分到頁面上,這個過程就是重繪(repaint)。

所以重排一定會引起重繪,而重繪不一定會引起重排,比如一個元素的改變並沒有影響佈局的改變(background-color的改變),在這種情況下,只會發生一個重繪(不需要重排)。

引起重排的因素

可以總結出,當元素的幾何屬性或頁面佈局發生改變就會引起重排,比如:

  • 對可見 DOM 元素的操作(新增,刪除或順序變化)
  • 元素位置發生改變
  • 元素的幾何屬性發生改變(比如:外邊距、內邊距、邊框寬度以及內容改變引起的寬高的改變)
  • 頁面首次渲染
  • 偽類樣式啟用(hover等)
  • 瀏覽器視口尺寸發生改變(滾動或縮放)

如何優化

重繪與重排都是代價昂貴的操作(因為每次重排都會產生計算消耗),它們會導致 web 應用的 UI 反應遲鈍,所以開發者在編寫應用程式的時候應當儘量減少這類過程的發生。

渲染樹佇列

因為過多的重繪與重排可能會導致應用的卡頓,所以瀏覽器會對這個有一個優化的過程。大多數瀏覽器會通過佇列化來批量執行(比如把指令碼對 DOM 的修改放入一個佇列,在佇列所有操作都結束後再進行一次繪製)。但是開發者有時可能不知不覺的強制重新整理渲染佇列來立即進行重排重繪,比如獲取頁面佈局資訊會導致渲染佇列的強制重新整理,以下屬性或方法會立即觸發頁面繪製:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()

以上屬性和方法都是要瀏覽器返回最新的佈局資訊,所以瀏覽器會立刻執行渲染佇列中的“待處理變化”, 並觸發重排重繪然後返回最新的值。所以在修改樣式的過程中,應該儘量避免使用以上屬性和方法。

減少重繪與重排

為了減少重繪重排的發生次數,開發者應該合併多次對 DOM 的修改和對樣式的修改,然後一次性處理。

合併樣式操作

比如:

var el = document.querySelector(`div`);
el.style.borderLeft = `1px`;
el.style.borderRight = `2px`;
el.style.padding = `5px`;
複製程式碼

可以合併成:

var el = document.querySelector(`div`);
el.style.cssText = `border-left: 1px; border-right: 1px; padding: 5px;`
複製程式碼

批量修改DOM

使元素脫離文件流,再對其進行操作,然後再把元素帶回文件中,這種辦法可以有效減少重繪重排的次數。有三種基本辦法可以使元素脫離文件流:

隱藏元素,應用修改,重新顯示
var ul = document.querySelector(`ul`);
ul.style.display = `none`;
// code... 對ul進行DOM操作
ul.style.display = `block`;
複製程式碼
使用文件片段(document fragment),構建一個空白文件進行 DOM 操作,然後再放回原文件中

var fragment = document.createDocumentFragment();
// code... 對fragment進行DOM操作
var ul = document.querySelector(`ul`);
ul.appendChild(fragment)
複製程式碼
拷貝要修改的元素到一個脫離文件流的節點中,修改副本,然後再替換原始元素
var ul = document.querySelector(`ul`);
var cloneUl = ul.cloneNode(true);
// code... 對clone節點進行DOM操作
ul.parentNode.replaceChild(cloneUl, ul)
複製程式碼

快取佈局資訊

前面已經知道,獲取頁面佈局資訊,會導致瀏覽器強制重新整理渲染佇列。所以減少這些操作是非常有必要的,開發者可以將第一次獲取到的頁面資訊快取到區域性變數中,然後再操作區域性變數,比如下面的虛擬碼示例:

// 低效的
element.style.left = 1 + element.offsetLeft + `px`;
element.style.top = 1 + element.offsetTop + `px`;
if (element.offsetTop > 500) {
    stopAnimation();
}
// 高效的
var offsetLeft = element.offsetLeft;
var offsetTop = element.offsetTop;
offsetLeft++;
offsetTop++;
element.style.left = offsetLeft + `px`;
element.style.top = offsetTop + `px`;
if (offsetTop > 500) {
    stopAnimation();
}
複製程式碼

總結

為了減少重繪重排帶來的效能消耗,可以通過以下幾點改善 web 應用:

  1. 批量修改 DOM 和樣式
  2. “離線”操作 DOM 樹,脫離文件流
  3. 快取到區域性變數,減少頁面佈局資訊的訪問次數

參考

高效能JavaScript

相關文章