摘要:要想減少迴流和重繪的次數,首先要瞭解迴流和重繪是如何觸發的。 本文分享自華為雲社區《前端頁面之“迴流重繪”》,作者:CoderBin。 “迴流重繪”是什麼? 在HTML中,每個元素都可以理解成一個盒子,在瀏覽器解析過程中,會涉及到迴流與重繪: 迴流:佈局引擎會根據各種樣式計算每個盒子在頁面上的 ...
摘要:要想減少迴流和重繪的次數,首先要瞭解迴流和重繪是如何觸發的。
本文分享自華為雲社區《前端頁面之“迴流重繪”》,作者: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>
前者每次單獨操作,都去觸發一次渲染樹更改(新瀏覽器不會),都去觸發一次渲染樹更改,從而導致相應的迴流與重繪過程,合併之後,等於我們將所有的更改一次性發出。
我們還可以通過通過設置元素屬性display: none,將其從頁面上去掉,然後再進行後續操作,這些後續操作也不會觸發迴流與重繪,這個過程稱為離線操作。