天天看點

帶你認識什麼是“回流重繪”

摘要:要想減少回流和重繪的次數,首先要了解回流和重繪是如何觸發的。

本文分享自華為雲社群《​​前端頁面之“回流重繪”​​》,作者:CoderBin。

“回流重繪”是什麼?

在HTML中,每個元素都可以了解成一個盒子,在浏覽器解析過程中,會涉及到回流與重繪:

  • 回流:布局引擎會根據各種樣式計算每個盒子在頁面上的大小與位置;
  • 重繪:當計算好盒模型的位置、大小及其他屬性後,浏覽器根據每個盒子特性進行繪制。

具體的浏覽器解析渲染機制如下所示:

帶你認識什麼是“回流重繪”
  • 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
  • 将DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
  • Layout(回流):根據生成的渲染樹,進行回流(Layout),得到節點的幾何資訊(位置,大小)
  • Painting(重繪):根據渲染樹以及回流得到的幾何資訊,得到節點的絕對像素
  • Display:将像素發送給GPU,展示在頁面上

在頁面初始渲染階段,回流不可避免的觸發,可以了解成頁面一開始是空白的元素,後面添加了新的元素使頁面布局發生改變。

當我們對 DOM 的修改引發了 DOM幾何尺寸的變化(比如修改元素的寬、高或隐藏元素等)時,浏覽器需要重新計算元素的幾何屬性,然後再将計算的結果繪制出來。

當我們對 DOM的修改導緻了樣式的變化(color或background-color),卻并未影響其幾何屬性時,浏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式,這裡就僅僅觸發了回流。

如何觸發

要想減少回流和重繪的次數,首先要了解回流和重繪是如何觸發的。

回流觸發時機

回流這一階段主要是計算節點的位置和幾何資訊,那麼當頁面布局和幾何資訊發生變化的時候,就需要回流,如下面情況:

  • 添加或删除可見的DOM元素
  • 元素的位置發生變化
  • 元素的尺寸發生變化(包括外邊距、内邊框、邊框大小、高度和寬度等)
  • 内容發生變化,比如文本變化或圖檔被另一個不同尺寸的圖檔所替代
  • 頁面一開始渲染的時候(這避免不了)
  • 浏覽器的視窗尺寸變化(因為回流是根據視口的大小來計算元素的位置和大小的)

還有一些容易被忽略的操作:擷取一些特定屬性的值。

offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

這些屬性有一個共性,就是需要通過即時計算得到。是以浏覽器為了擷取這些值,也會進行回流。

除此還包括getComputedStyle方法,原理是一樣的。

重繪觸發時機

觸發回流一定會觸發重繪

可以把頁面了解為一個黑闆,黑闆上有一朵畫好的小花。現在我們要把這朵從左邊移到了右邊,那我們要先确定好右邊的具體位置,畫好形狀(回流),再畫上它原有的顔色(重繪)。

除此之外還有一些其他引起重繪行為:

  • 顔色的修改
  • 文本方向的修改
  • 陰影的修改

浏覽器優化機制

由于每次重排都會造成額外的計算消耗,是以大多數浏覽器都會通過隊列化修改并批量執行來優化重排過程。浏覽器會将修改操作放入到隊列裡,直到過了一段時間或者操作達到了一個門檻值,才清空隊列。

當你擷取布局資訊的操作的時候,會強制隊列重新整理,包括前面講到的offsetTop等方法都會傳回最新的資料。

是以浏覽器不得不清空隊列,觸發回流重繪來傳回正确的值。

如何減少

我們了解了如何觸發回流和重繪的場景,下面給出避免回流的經驗:

  • 如果想設定元素的樣式,通過改變元素的 class 類名 (盡可能在 DOM 樹的最裡層)
  • 避免設定多項内聯樣式
  • 應用元素的動畫,使用 position 屬性的 fixed 值或 absolute 值(如前文示例所提)
  • 避免使用 table 布局,table 中每個元素的大小以及内容的改動,都會導緻整個 table 的重新計算
  • 對于那些複雜的動畫,對其設定 position: fixed/absolute,盡可能地使元素脫離文檔流,進而減少對其他元素的影響
  • 使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起回流重繪
  • 避免使用 CSS 的 JavaScript 表達式

在使用 JavaScript 動态插入多個節點時, 可以使用DocumentFragment. 建立後一次插入. 就能避免多次的渲染性能。

但有時候,我們會無可避免地進行回流或者重繪,我們可以更好使用它們

例如,多次修改一個把元素布局的時候,我們很可能會如下操作。

const el = document.getElementById('el')
for(let i=0;i<10;i++) {
 el.style.top = el.offsetTop + 10 + "px";
 el.style.left = el.offsetLeft + 10 + "px";
}      

每次循環都需要擷取多次offset屬性,比較糟糕,可以使用變量的形式緩存起來,待計算完畢再送出給浏覽器發出重計算請求。

// 緩存offsetLeft與offsetTop的值
const el = document.getElementById('el') 
let offLeft = el.offsetLeft, offTop = el.offsetTop
// 在JS層面進行計算
for(let i=0;i<10;i++) {
 offLeft += 10
 offTop += 10
}
// 一次性将計算結果應用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop + "px"      

我們還可避免改變樣式,使用類名去合并樣式。

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'      

使用類名去合并樣式

<style>
 .basic_style {
 width: 100px;
 height: 200px;
 border: 10px solid red;
 color: red;
  }
</style>
<script>
 const container = document.getElementById('container')
 container.classList.add('basic_style')
</script>      

前者每次單獨操作,都去觸發一次渲染樹更改(新浏覽器不會),都去觸發一次渲染樹更改,進而導緻相應的回流與重繪過程,合并之後,等于我們将所有的更改一次性發出。