天天看點

讀書筆記 --- > 再次閱讀回流與重繪

參考 - 強烈推薦看看,這個作者寫了很多特别好的文章.

浏覽器渲染過程
  1. 解析HTML,生成DOM樹; 解析CSS生成CSSOM樹
  2. 将DOM樹和CSSOM樹合并,生成渲染(Render)樹
  3. Layout(回流): 根據生成的渲染樹,視口(viewport),得到節點的幾何資訊(位置、大小)
  4. Painting(重繪): 根據渲染樹和幾何資訊得到節點的絕對像素
  5. Display: 将像素發送給GPU,展示在頁面上
生成渲染樹
讀書筆記 --- > 再次閱讀回流與重繪

為了建構渲染樹,浏覽器主要完成了以下工作:

  1. 從DOM樹的根節點開始周遊每個可見節點
  2. 對于每個可見的節點,找到CSSOM樹中的規則,并應用它們
  3. 根據每個可見節點及其對應的樣式,組合生成渲染樹

【不可見的節點】:

  • 一些不會渲染輸出的節點: 比如script、meta、link等
  • 一些通過css進行隐藏的節點。比如display: none。注意,利用visibility和opacity隐藏的節點,還是會顯示在渲染樹上的。隻有display:none的節點才不會顯示在渲染樹上

【注意】: 渲染樹隻包括可見的節點

回流(Layout)

前面将DOM節點以及它對應的樣式結合起來,可是我們還需要計算它們在裝置視口(viewport)内的确切位置和大小,這個計算的階段就是回流。看下面的栗子:

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

我們可以看到,第一個div将節點的顯示尺寸設定為視口寬度的50%,第二個div将其尺寸設定為父節點的50%.而在回流這個階段,我們就需要根據視口具體的寬度,将其轉為實際的像素值。

讀書筆記 --- &gt; 再次閱讀回流與重繪
重繪 (Painting)
  • 生成渲染樹階段: 我們直到了哪些節點是可見的以及可見節點的樣式
  • 在回流階段: 我們得到了可見元素的具體幾何資訊
我們得到的資訊,最終都會托付給GPU進行渲染

GPU的渲染需要具體的像素位置,這就是重繪階段所做的事情: 根據渲染樹和幾何資訊計算出絕對像素點.

何時發生回流重繪

回流主要是計算節點的幾何位置和幾何像素大小.那麼當頁面布局和幾何資訊發生變化的時候,就需要回流:

  • 添加或删除可見的DOM元素
  • 元素的位置發生變化
  • 元素的尺寸發生變化(内/外邊距、邊框大小、高度和寬度等)
  • 内容發生: 文本發生變化或圖檔被另一個不同尺寸的圖檔所替代
  • 頁面剛開始渲染的時候
  • 浏覽器的視窗尺寸變化: 回流是根據視口的大小來計算元素的位置和大小的
經典老話: 回流一定重繪,重繪不一定回流
浏覽器的優化機制

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

還有一些強制重新整理的屬性(避免使用):

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect
前端優化

1 -【并多次的DOM和添加樣式】

// 未優化前 - 3次
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'

// 添加樣式 - 1次
const el = document.getElementById('test');
el.calssName += ' active';
           

2 -【脫離文檔流】

當元素脫離文檔流後,對元素的所有操作都不會引起回流和重繪.是以如果,對某個元素進行的DOM操作比較多的時候,可以先将元素脫離文檔流,然後操作,最後在放回文檔流。具體操作如下:

  1. 使元素脫離文檔流
  2. 對其進行多次修改
  3. 将元素帶回到文檔中.

[注] : 上述的1、3會引起回流和重繪.

【脫離文檔流的方法】

  • 隐藏元素,修改應用,重新顯示
  • 使用文檔片段(document fragment)在使用DOM之外建構一個子樹,再把它拷貝回文檔
  • 将原始元素拷貝到一個脫離文檔的節點中,修改節點後,再替換原始的元素。
// 每次插入li都會引起一次回流和重繪
function appendDataToElement(appendToElement, data) {
    let li;
    for(let i =0,len = data.length;i < len;i++){
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appdenDataToElement(ul, data);
           

[隐藏元素]

// 僅在隐藏元素和現實元素時産生2次回流和重繪
function appendDataToElement(appendToElement, data) {
    let li;
    for(let i =0, len = data.length; i < len; 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';
           

[使用文檔片段] - 在目前DOM外建構一個子樹,再把它拷貝回文檔

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

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

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

[注] - 現代浏覽器使用了隊列來存儲多次修改,是以上述的優化可能效果不是很理想.

3 - 【避免觸發同步布局事件】

// 栗子: 多次使用到 offsetWidth 屬性
function initP(){
    for(let i = 0; i< paragraph.length; i++){
        paragraph[i].style.width = box.offsetWidth + 'px'
    }
}
           

上述代碼每次循環,都會使浏覽器強制重新整理隊列(

box.offsetWidth

),造成多次回流和重繪.改進如下:

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

4 - 【複雜動畫的優化】

對于複雜動畫效果,由于會經常的引起回流和重繪。是以,我們可以使用絕對定位,讓它脫離文檔流。否則會引起父元素以及後續元素頻繁的回流 - 栗子

繼續閱讀