迴流和重繪(轉載)

迴流和重繪能夠說是每個web開發者都常常聽到的兩個詞語,我也不例外,但是一直不是很清楚這兩步具體作了什麼事情。最近因爲部門內部要作分享,因此對其進行了一些研究,看了一些博客和書籍,整理了一些內容而且結合本身的體會,寫了這篇文章,但願能夠幫助到你們。javascript

瀏覽器的渲染過程

本文先從瀏覽器的渲染過程來從頭至尾的講解一下回流重繪,若是你們想直接看如何減小回流和重繪,能夠跳到後面。(這個渲染過程來自MDNcss

webkit渲染過程

從上面這個圖上,咱們能夠看到,瀏覽器渲染過程以下:html

  1. 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
  2. 將DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
  3. Layout(迴流):根據生成的渲染樹,進行迴流(Layout),獲得節點的幾何信息(位置,大小)
  4. Painting(重繪):根據渲染樹以及迴流獲得的幾何信息,獲得節點的絕對像素
  5. Display:將像素髮送給GPU,展現在頁面上。(這一步其實還有不少內容,好比會在GPU將多個合成層合併爲同一個層,並展現在頁面中。而css3硬件加速的原理則是新建合成層,這裏咱們不展開,以後有機會會寫一篇博客)

渲染過程看起來很簡單,讓咱們來具體瞭解下每一步具體作了什麼。java

生成渲染樹

爲了構建渲染樹,瀏覽器主要完成了如下工做:css3

  1. 從DOM樹的根節點開始遍歷每一個可見節點。
  2. 對於每一個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
  3. 根據每一個可見節點以及其對應的樣式,組合生成渲染樹。

第一步中,既然說到了要遍歷可見的節點,那麼咱們得先知道,什麼節點是不可見的。不可見的節點包括:git

  • 一些不會渲染輸出的節點,好比script、meta、link等。
  • 一些經過css進行隱藏的節點。好比display:none。注意,利用visibility和opacity隱藏的節點,仍是會顯示在渲染樹上的。只有display:none的節點纔不會顯示在渲染樹上。

注意:渲染樹只包含可見的節點github

迴流

前面咱們經過構造渲染樹,咱們將可見DOM節點以及它對應的樣式結合起來,但是咱們還須要計算它們在設備視口(viewport)內的確切位置和大小,這個計算的階段就是迴流。web

爲了弄清每一個對象在網站上的確切大小和位置,瀏覽器從渲染樹的根節點開始遍歷,咱們能夠如下面這個實例來表示:算法

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

咱們能夠看到,第一個div將節點的顯示尺寸設置爲視口寬度的50%,第二個div將其尺寸設置爲父節點的50%。而在迴流這個階段,咱們就須要根據視口具體的寬度,將其轉爲實際的像素值。(以下圖)chrome

重繪

最終,咱們經過構造渲染樹和迴流階段,咱們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何信息(位置、大小),那麼咱們就能夠將渲染樹的每一個節點都轉換爲屏幕上的實際像素,這個階段就叫作重繪節點。

既然知道了瀏覽器的渲染過程後,咱們就來探討下,什麼時候會發生迴流重繪。

什麼時候發生迴流重繪

咱們前面知道了,迴流這一階段主要是計算節點的位置和幾何信息,那麼當頁面佈局和幾何信息發生變化的時候,就須要迴流。好比如下狀況:

  • 添加或刪除可見的DOM元素
  • 元素的位置發生變化
  • 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
  • 內容發生變化,好比文本變化或圖片被另外一個不一樣尺寸的圖片所替代。
  • 頁面一開始渲染的時候(這確定避免不了)
  • 瀏覽器的窗口尺寸變化(由於迴流是根據視口的大小來計算元素的位置和大小的)

注意:迴流必定會觸發重繪,而重繪不必定會迴流

根據改變的範圍和程度,渲染樹中或大或小的部分須要從新計算,有些改變會觸發整個頁面的重排,好比,滾動條出現的時候或者修改了根節點。

瀏覽器的優化機制

現代的瀏覽器都是很聰明的,因爲每次重排都會形成額外的計算消耗,所以大多數瀏覽器都會經過隊列化修改並批量執行來優化重排過程。瀏覽器會將修改操做放入到隊列裏,直到過了一段時間或者操做達到了一個閾值,才清空隊列。可是!當你獲取佈局信息的操做的時候,會強制隊列刷新,好比當你訪問如下屬性或者使用如下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect
  • 具體能夠訪問這個網站:https://gist.github.com/paulirish/5d52fb081b3570c81e3a

以上屬性和方法都須要返回最新的佈局信息,所以瀏覽器不得不清空隊列,觸發迴流重繪來返回正確的值。所以,咱們在修改樣式的時候,最好避免使用上面列出的屬性,他們都會刷新渲染隊列。若是要使用它們,最好將值緩存起來。

減小回流和重繪

好了,到了咱們今天的重頭戲,前面說了這麼多背景和理論知識,接下來讓咱們談談如何減小回流和重繪。

最小化重繪和重排

因爲重繪和重排可能代價比較昂貴,所以最好就是能夠減小它的發生次數。爲了減小發生次數,咱們能夠合併屢次對DOM和樣式的修改,而後一次處理掉。考慮這個例子

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

例子中,有三個樣式屬性被修改了,每個都會影響元素的幾何結構,引發迴流。固然,大部分現代瀏覽器都對其作了優化,所以,只會觸發一次重排。可是若是在舊版的瀏覽器或者在上面代碼執行的時候,有其餘代碼訪問了佈局信息(上文中的會觸發迴流的佈局信息),那麼就會致使三次重排。

所以,咱們能夠合併全部的改變而後依次處理,好比咱們能夠採起如下的方式:

  • 使用cssText

    const el = document.getElementById('test');
    el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
  • 修改CSS的class

    const el = document.getElementById('test');
    el.className += ' active';

批量修改DOM

當咱們須要對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);

若是咱們直接這樣執行的話,因爲每次循環都會插入一個新的節點,會致使瀏覽器迴流一次。

咱們可使用這三種方式進行優化:

隱藏元素,應用修改,從新顯示

這個會在展現和隱藏節點的時候,產生兩次重繪

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';

使用文檔片斷(document fragment)在當前DOM以外構建一個子樹,再把它拷貝迴文檔

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

將原始元素拷貝到一個脫離文檔的節點中,修改節點後,再替換原始的元素。

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);

對於上述那種狀況,我寫了一個demo來測試修改前和修改後的性能。然而實驗結果不是很理想。

緣由:緣由其實上面也說過了,瀏覽器會使用隊列來儲存屢次修改,進行優化,因此對這個優化方案,咱們其實不用優先考慮。

避免觸發同步佈局事件

上文咱們說過,當咱們訪問元素的一些屬性的時候,會致使瀏覽器強制清空隊列,進行強制同步佈局。舉個例子,好比說咱們想將一個p標籤數組的寬度賦值爲一個元素的寬度,咱們可能寫出這樣的代碼:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

這段代碼看上去是沒有什麼問題,但是其實會形成很大的性能問題。在每次循環的時候,都讀取了box的一個offsetWidth屬性值,而後利用它來更新p標籤的width屬性。這就致使了每一次循環的時候,瀏覽器都必須先使上一次循環中的樣式更新操做生效,才能響應本次循環的樣式讀取操做。每一次循環都會強制瀏覽器刷新隊列。咱們能夠優化爲:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

一樣,我也寫了個demo來比較二者的性能差別。你能夠本身點開這個demo體驗下。這個對比差距就比較明顯。

對於複雜動畫效果,使用絕對定位讓其脫離文檔流

對於複雜動畫效果,因爲會常常的引發迴流重繪,所以,咱們可使用絕對定位,讓它脫離文檔流。不然會引發父元素以及後續元素頻繁的迴流。這個咱們就直接上個例子

打開這個例子後,咱們能夠打開控制檯,控制檯上會輸出當前的幀數(雖然不許)。

從上圖中,咱們能夠看到,幀數一直都沒到60。這個時候,只要咱們點擊一下那個按鈕,把這個元素設置爲絕對定位,幀數就能夠穩定60。

css3硬件加速(GPU加速)

比起考慮如何減小回流重繪,咱們更指望的是,根本不要回流重繪。這個時候,css3硬件加速就閃亮登場啦!!

劃重點:使用css3硬件加速,可讓transform、opacity、filters這些動畫不會引發迴流重繪 。可是對於動畫的其它屬性,好比background-color這些,仍是會引發迴流重繪的,不過它仍是能夠提高這些動畫的性能。

本篇文章只討論如何使用,暫不考慮其原理,以後有空會另外開篇文章說明。

如何使用

常見的觸發硬件加速的css屬性:

  • transform
  • opacity
  • filters
  • Will-change

效果

咱們能夠先看個例子。我經過使用chrome的Performance捕獲了一段時間的迴流重繪狀況,實際結果以下圖:

從圖中咱們能夠看出,在動畫進行的時候,沒有發生任何的迴流重繪。若是感興趣你也能夠本身作下實驗。

重點

  • 使用css3硬件加速,可讓transform、opacity、filters這些動畫不會引發迴流重繪
  • 對於動畫的其它屬性,好比background-color這些,仍是會引發迴流重繪的,不過它仍是能夠提高這些動畫的性能。

css3硬件加速的坑

  • 若是你爲太多元素使用css3硬件加速,會致使內存佔用較大,會有性能問題。

  • 在GPU渲染字體會致使抗鋸齒無效。這是由於GPU和CPU的算法不一樣。所以若是你不在動畫結束的時候關閉硬件加速,會產生字體模糊。

總結

本文主要講了瀏覽器的渲染過程、瀏覽器的優化機制以及如何減小甚至避免迴流和重繪,但願能夠幫助你們更好的理解迴流重繪。

參考文獻

注:本文來自於https://www.cnblogs.com/chenjg/p/10099886.html#4138218

相關文章
相關標籤/搜索