[前端] 重排和重繪
網頁的生成過程
- 解析HTML,生成DOM樹。
- 解析CSS,生成CSSOM樹。
- 結合DOM樹和CSSOM樹,為每一個節點計算CSS屬性,生成渲染樹,RenderTree。
- 生成布局(Flow),計算渲染樹上所有節點的位置。
- 將布局繪制(Paint)到屏幕上。
- 布局生成和繪制的過程就是渲染。
- 網頁生成的時候至少渲染一次。
- 用戶交互可能導致重新渲染。
- 渲染是耗時的,應減少不必要的重新渲染以提高網頁性能。
重排和重繪的概念
- 重新生成布局,就叫重排(Reflow),也叫回流。
- 重新繪制,就是重繪(Repaint)。
由于布局生成和繪制存在先后順序關系,重排必定導致重繪,但重繪不一定需要重排。
重排 Reflow
重排與布局有關,當布局發生變化時,也就是元素的幾何信息(DOM節點的尺寸和位置)發生變化時,將會觸發重排,重新計算元素的幾何位置,然后重新繪制。
常見引起重排的屬性和方法
- 添加或刪除可見的DOM元素;
- 元素尺寸改變——邊距、填充、邊框、寬度高度;
- 內容變化,比如input框中輸入文字;
- 瀏覽器窗口尺寸改變——resize事件發生時;
- 讀取offsetWidth、offsetHeight等屬性(瀏覽器為確保數據準確性,會先強制重排);
- 設置style屬性的值(改變了元素的尺寸或位置);
重排影響范圍
-
全局范圍重排:從根節點
html開始對整個渲染樹進行重排;當一個DOM節點變小,它后續的元素可能位置也發生變化,而它的父元素如果沒有固定寬高也可能發生收縮,因此:
一個節點的重排可能影響到它的相鄰節點、父節點......從而引發全局范圍的重排。
-
局部范圍重排:對渲染樹的某部分或某一個渲染對象進行重排。
將一個DOM的幾何信息固定,當其內部的節點發生重排,則不會影響到外部的節點,是局部范圍的重排。
重排總結
- 重排的性能開銷與渲染樹上需要重新構建的節點數有關。
- 盡量以局部布局的形式組織
html,將重排控制在局部范圍內。
重繪 Repaint
一個元素的外觀發生改變,但不改變布局,不影響周圍的元素,只是重新繪制該元素的外觀,這個過程叫做重繪。
常見引起重繪的屬性和方法
colorborder-stylebackgroundvisibilityborder-radiusbox-shadowoutlinetext-decoration
渲染隊列機制
當元素的幾何屬性被修改,不會立刻導致重排。這個操作會被放到渲染隊列,等到隊列中的操作到達一定數量或者達到一定的時間間隔,瀏覽器才會批量執行這些操作。
如下的代碼執行了四次對于元素的幾何屬性的修改,但是只會觸發一次重排。
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
強制刷新隊列
當訪問與布局相關的屬性時,瀏覽器為確保其準確性、準時性,會強制立即執行渲染隊列中的任務,即立即重排和重繪。
如下這段代碼會觸發4次重排和重繪:
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);
強制刷新隊列的屬性和方法
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle(), 或者 IE的 currentStyle
在開發過程中應該盡可能少地訪問這些屬性,避免多次重排。
重排優化策略
1. 分離讀寫操作
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);
統一讀,再統一寫,而不是邊讀邊寫。這樣可以將重排次數降低到1次。第一個console.log將渲染隊列清空,觸發一次重排,而后續的console.log由于渲染隊列本身是空的因此不會觸發重排。
2. 樣式集中改變
不要分批次的修改樣式屬性,而是一次性修改。
建議提前寫好若干個CSS的class選擇器,然后 JS 負責切換。
// bad
box.style.left = '10px';
box.style.top = '200px';
box.style.transform = 'scale(1.1)';
// good
box.classList.add('className1');
box.classList.remove('className2');
3. 緩存布局信息
布局信息的每一次讀取都會導致強制重排,如果確保布局信息在當前情景下不會有變動,可以使用一個臨時變量緩存使用。
// bad 強制刷新 觸發兩次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
// good
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
4. 離線修改DOM
DOM設置display: none;,將其從渲染樹中移除,然后再進行復雜修改操作,最后再將其顯示出來。
整個過程包含隱藏和顯示共兩次重排。
或者使用DocumentFragment創建一個DOM碎片,然后在其上面批量操作DOM,操作完成之后再添加到文檔中,這樣只會觸發一次重排。
5. 目標元素設置絕對定位
設置position為absolute或fixed,這樣就只會影響子節點的重排,不會影響外部。
6. 不要使用table布局
table中的某一個元素觸發重排將導致整個table重排。
如果是在維護遠古項目不得不使用table布局,可以設置table-layout: fixed;或者table-layout: auto;,這樣可以讓table一行一行的渲染,限制重排的影響范圍。

浙公網安備 33010602011771號