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

前言

現代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

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