1. 瀏覽器渲染機制
- 瀏覽器采用流式布局模型(
Flow Based Layout
) - 瀏覽器會把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合並就產生了渲染樹(Render Tree
)。 - 有了
RenderTree
,我們就知道了所有節點的樣式,然后計算他們在頁面上的大小和位置,最后把節點繪制到頁面上。 - 由於瀏覽器使用流式布局,對
Render Tree
的計算通常只需要遍歷一次就可以完成,但table
及其內部元素除外,他們可能需要多次計算,通常要花3倍於同等元素的時間,這也是為什么要避免使用table
布局的原因之一。
2. 重繪
由於節點的幾何屬性發生改變或者由於樣式發生改變而不會影響布局的,稱為重繪,例如outline
, visibility
, color
、background-color
等,重繪的代價是高昂的,因為瀏覽器必須驗證DOM樹上其他節點元素的可見性。
3. 回流
回流是布局或者幾何屬性需要改變就稱為回流。回流是影響瀏覽器性能的關鍵因素,因為其變化涉及到部分頁面(或是整個頁面)的布局更新。一個元素的回流可能會導致了其所有子元素以及DOM中緊隨其后的節點、祖先節點元素的隨后的回流。
<body> <div class="error"> <h4>我的組件</h4> <p><strong>錯誤:</strong>錯誤的描述…</p> <h5>錯誤糾正</h5> <ol> <li>第一步</li> <li>第二步</li> </ol> </div> </body>
在上面的HTML片段中,對該段落p標簽回流將會引發強烈的回流,因為它是一個子節點。這也導致了祖先的回流。此外,<h5>和<ol>也會有簡單的回流,因為其在DOM中的回流元素之后。大部分的回流將導致頁面的重新渲染。
回流必定會發生重繪,重繪不一定會引發回流。
4.瀏覽器優化
應該避免頻繁使用以下屬性,它們都會強制渲染刷新隊列。
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- width,height
- getComputedStyle()
- getBoundingClientRect()
5.減少重繪和回流
1.CSS
- 使用transform替代top
- 使用visibility替換display:none,因為前者只會引起重繪,后者會引發回流(改變了布局)
- 避免使用table布局,可能很小的一個改動會造成整個table的重新布局
- 盡可能在DOM樹的最末端改變class,回流是不可避免的,但可以減少其影響。盡可能在DOM樹的最末端改變class,可以限制了回流的范圍。
- 避免設置多層內聯樣式,css選擇符從右往左匹配查找,避免節點層級過多。
- 將動畫效果應用到position屬性為absolute或者fixed的元素上,避免影響其他元素的布局,這樣只是一個重繪,而不是回流。同時,控制動畫速度可以選擇requestAnimationFrame。
- 避免使用css表達式,可能會引發回流。
- 將頻繁重繪或者回流的節點設置為圖層,圖層能夠阻止該節點的渲染行為影響別的節點,例如will-change,video,iframe等標簽,瀏覽器會自動將該節點變為圖層。
- CSS3硬件加速(GPU加速),使用CSS3硬件加速,可以讓transform,opacity,filters這些動畫不會引起回流重繪。
2.JavaScript
- 避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class並一次性更改class屬性
- 避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最后再把它添加到文檔中。
- 避免頻繁讀取會引發回流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
- 對具有復雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及后續元素頻繁回流。
何時發生回流重繪
當頁面布局和幾何信息發生變化的時候,就需要回流。
- 添加或刪除可見的DOM元素
- 元素的位置發生變化
- 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
- 內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代。
- 頁面一開始渲染的時候(這肯定避免不了)
- 瀏覽器的窗口尺寸變化(因為回流是根據視口的大小來計算元素的位置和大小的)
注意:回流一定會觸發重繪,而重繪不一定會回流
根據改變的范圍和程度,渲染樹中或大或小的部分需要重新計算,有些改變會觸發整個頁面的重排,比如,滾動條出現的時候或者修改了根節點。
瀏覽器的優化機制
由於每次重排都會造成額外的計算消耗,因此大多數瀏覽器都會通過隊列化修改並批量執行來優化重排過程。瀏覽器會將修改操作放入到隊列里,直到過了一段時間或者操作達到了一個閾值,才清空隊列。
但是,當你獲取布局信息的操作的時候,會強制隊列刷新,比如當你訪問以下屬性或者使用以下方法:
- offsetTop offsetLeft offsetWidth offsetHeight
- scrollTop scrollLeft scrollWidth srcollHeight
- clientTop clientLeft clientWidth clientHeight
- getComputedStyle()
- getBoundingClientRect
以上屬性和方法都需要返回最新的布局信息,因此瀏覽器不得不清空隊列,觸發回流重繪來返回正確的值。因此,我們在修改樣式的時候,最好避免使用上面列出的屬性。如果要使用它們,最好將值緩存起來。
減少回流和重繪
最小化重繪和重排
合並多次對DOM和樣式的修改,一次性處理。
const el = document.getElementById('test'); el.style.padding = '5px'; el.style.borderLeft = '1px'; el.style.borderRight = '2px';
優化方案1:
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
優化方案2:
const el = document.getElementById('test');
el.className += ' active';
批量修改DOM
1.使元素脫離文檔流
2.對其進行多次修改
3.將元素帶回到文檔中
該過程的第一步和第三步可能會引起回流,但是經過第一步之后,對DOM的所有修改都不會引起回流,因為它已經不在渲染樹了。
有三種方式可以讓DOM脫離文檔流:
隱藏元素、應用修改、重新顯示
使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝回文檔
將原始元素拷貝到一個脫離文檔的節點中,修改節點后,再替換原始的元素。
考慮我們要執行一段批量插入節點的代碼:
function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); } } const ul = document.getElementById('list'); appendDataToElement(ul, data);
如果我們直接這樣執行的話,由於每次循環都會插入一個新的節點,會導致瀏覽器回流一次。
我們可以使用這三種方式進行優化
隱藏元素,應用修改,重新顯示
這個會在展示和隱藏節點的時候,產生兩次重繪
優化方案1:
function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); } } const ul = document.getElementById('list'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block';
優化方案2:
使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝回文檔
const ul = document.getElementById('list'); const fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); ul.appendChild(fragment);
優化方案3:
將原始元素拷貝到一個脫離文檔的節點中,修改節點后,再替換原始的元素
const ul = document.getElementById('list'); const clone = ul.cloneNode(true); appendDataToElement(clone, data); ul.parentNode.replaceChild(clone, ul);
參考文章:
https://github.com/sisterAn/blog/issues/33
https://github.com/chenjigeng/blog/issues/4