關於Reflow和Repaint

紀錄關於Reflow和Repaint的特性以及如何優化網頁


在前端開發中,再經果多次的開發修改後,總會不可避免的會遇到卡頓問題,這時我們就要做性能優化。而在優化時,我們會首先考慮畫面的渲染,因為這對使用者來說是最有感的。

所以了解和控制瀏覽器的渲染過程是十分重要的。而在渲染中,Reflow和Repaint是其中兩個蠻重要的關鍵概念,理解它們有助我們編寫更好的代碼,從而提升網頁的性能和用戶體驗。

理解瀏覽器的渲染過程

在介紹reflow和repaint前,先簡單的說下渲染過程。

下圖是Webkit的渲染流程,大部分的瀏覽器渲染過程如下 (Reference: 瀏覽器的運作方式 | Articles | web.dev):

  1. a) 解析HTML檔案,生成DOM tree
  2. b) 解析CSS檔案,生成CSSOM
  3. 將DOM與CSSOM合併為Render Tree
  4. 計算每個可見元素的佈局,像是寬高和位置等等。
    • 如果遇到display:none的元素,則不會被計算進去,但該元素是會被構建DOM Tree的
  5. 將Render Tree計算結果繪製到畫面上

而Reflow和Repaint則會在第三和第四個步驟觸發。

一、什麼是Reflow?

1.1 定義

Reflow,又稱回流,是指當DOM元素的幾何屬性(例如尺寸、位置)發生變化時,瀏覽器需要重新計算整個頁面的佈局。這是一個代價高昂的過程,因為它可能影響到整個頁面或頁面中的大量元素。

1.2 觸發Reflow的因素

  1. 添加或移除DOM元素
  2. 修改元素的尺寸或邊距
  3. 改變瀏覽器窗口的大小
  4. 應用CSS樣式的改變
  5. 使用JavaScript操作元素的樣式屬性

1.3 Reflow的影響

Reflow是一個同步的、不可避免的過程,並且是性能瓶頸之一。當頁面中有大量的Reflow時,會導致頁面卡頓,影響用戶體驗。

1.4 重點事項

  • 頁面第一次載入時,一定會觸發一次Reflow
  • Reflow必定會觸發Repaint

二、什麼是Repaint?

2.1 定義

Repaint,又稱重繪,是指當元素的外觀(例如顏色、背景)發生變化,但尺寸和位置不變時,瀏覽器需要重新繪製元素。Repaint的代價相對較低,因為它只涉及到渲染樹的某些部分。

2.2 觸發Repaint的因素

  1. 更改元素的背景色、邊框色
  2. 修改文字顏色
  3. 改變透明度
  4. 應用CSS樣式的改變

2.3 Repaint的影響

雖然Repaint比Reflow的開銷要小,但頻繁的Repaint仍然會影響頁面的渲染性能。特別是在移動設備上,Repaint的代價會更高,因為這些設備的處理能力有限。

三、如何優化Reflow和Repaint

3.1 優化Reflow的方法

  1. 避免逐個修改樣式 將多個樣式改變合併為一次性修改。例如,使用class替代多次修改單個樣式屬性。

    // bad
    element.style.width = '100px';
    element.style.height = '100px';
    element.style.margin = '10px';
     
    // good
    element.className = 'new-style';
  2. 批量操作DOM 使用documentFragmentcloneNode等方法一次性對多個元素進行操作,然後再將結果插入到DOM中。

    // 使用 documentFragment
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      const div = document.createElement('div');
      div.className = 'new-element';
      fragment.appendChild(div);
    }
    document.body.appendChild(fragment);
  3. 減少使用浮動布局 浮動元素會影響其後所有元素的布局,應儘量避免頻繁使用浮動布局,改用Flexbox或Grid布局。

  4. 避免觸發同步布局 使用offsetWidthoffsetHeight等屬性會強制瀏覽器同步計算佈局,應儘量避免頻繁使用這些屬性。

    // bad
    const width = element.offsetWidth;
    element.style.width = width + 10 + 'px';
     
    // good
    element.style.width = element.offsetWidth + 10 + 'px';
  5. 脫離文件流 如果該DOM元素有設定動畫,可以透過將position改為absolute或是fixed,來脫離文件流,避免影響到其他元素的佈局。

  6. 用visibility代替display visibility: hidden仍會保留元素在文檔中的空間,而display: none會移除元素並觸發Reflow。因此,在適當的情況下,使用visibility可以避免Reflow。

3.2 優化Repaint的方法

  1. 使用CSS3硬件加速 使用transformopacity等屬性可以觸發硬件加速,減少Repaint的開銷。

    .accelerated {
      transform: translateZ(0);
    }
  2. 避免頻繁改變樣式 將頻繁變化的樣式提取到單獨的class中,通過切換class來改變樣式。

    // 壞例子
    element.style.backgroundColor = 'red';
    element.style.borderColor = 'blue';
     
    // 好例子
    element.className = 'highlight';
  3. 避免使用高代價的CSS屬性 某些CSS屬性,如box-shadowborder-radius等,會導致頻繁的Repaint,應避免在高頻操作中使用這些屬性。

四、實戰案例分析

案例: 大型列表的渲染

在渲染大量列表元素時,頻繁的 Reflow 和 Repaint 會嚴重影響性能。以下是兩種渲染方式的對比。

1. 效能差的渲染方式

下面直接將每個新創建的元素附加到 DOM 中,每次附加元素都會觸發頁面 Reflow 和 Repaint,導致性能下降。

<body>
  <button id="add-items">添加列表</button>
  <div class="list"></div>
 
  <script>
    document.getElementById('add-items').addEventListener('click', () => {
      const list = document.querySelector('.list');
      for (let i = 0; i < 100000; i++) {
        const item = document.createElement('div');
        item.className = 'list-item';
        item.textContent = `Item ${i}`;
        list.appendChild(item);
      }
    });
  </script>
</body>

我們打開Chrome的Devtool並使用Performance,點擊錄製後,點擊"添加列表" button。

我們能看到Layout約莫811.05ms

接著試著去記錄滾動,能看到有許多的paint部分(綠色)

2. 使用 Document Fragment 的優化渲染方式

為了提升性能,可以使用 Document Fragment。它是一個輕量的 DOM,只會存在於記憶體中,可以在記憶體中操作子節點,最後再將其附加到即時的 DOM 上。

由於 Fragment 位於記憶體中,而不是主 DOM Tree 的一部分,因此向其附加子級不會導致頁面 Repaint。 (See Document: createDocumentFragment() method - Web APIs | MDN)

<body>
  <button id="add-items">添加列表</button>
  <div class="list"></div>
 
  <script>
    document.getElementById('add-items').addEventListener('click', () => {
      const list = document.querySelector('.list');
      const buffer = document.createDocumentFragment();
      for (let i = 0; i < 100000; i++) {
        const item = document.createElement('div');
        item.className = 'list-item';
        item.textContent = `Item ${i}`;
        buffer.appendChild(item);
      }
      list.appendChild(buffer);
    });
  </script>
</body>

我們也繼續去紀錄Performance,點開時layout約莫774.28ms

接著去記錄滾動該頁面

通過使用performance,我們能看到頁面的Reflow和Repaint次數會顯著減少,從而提高渲染性能。

五、結論

理解和掌握Reflow和Repaint的概念,是讓我們理解如何提升頁面性能。通過合理的優化策略,我們可以顯著降低這些過程的開銷,從而提升網頁的響應速度和用戶體驗。