前一章談了 HTML 控制 JavaScript 載入順序的方法,設定得宜可提升不少效能。本章聚焦影響串接樣式表(CSS, Cascading Style Sheets)效能的兩位兇手:回流(Reflow)與重繪(Repaint)。

進入正題前,先認識瀏覽器的渲染機制。

瀏覽器的渲染步驟#

瀏覽器解析網頁的流程可拆解為幾個步驟:

  1. 從 HTML 檔解析出文件物件模型(DOM, Document Object Model)Tree
  2. 從 CSS 檔解析出 CSS 物件模型(CSSOM)Tree
  3. 兩者疊加後產生 Render Tree
  4. Reflow:計算出 Render Tree 上各個元素的物理屬性,如位置、大小、及是否看得見(visible)
  5. Repaint:將計算結果轉為實際的像素,畫到畫面上

Reflow#

此步驟由 Render Tree 的根節點出發,逐步計算每一個元素的位置、大小、是否被其他元素遮擋等屬性,需要耗費大量運算資源。

只要可能牽動這些屬性的操作,都會觸發 Reflow:

  • 設定 CSS 屬性
    • 大小:widthheight
    • 浮動:float
    • 定位:position
  • 使用者進行互動
    • 調整瀏覽器視窗大小
    • 輸入框的內容變更
  • JavaScript DOM 操作
  • 動態載入 CSS 樣式表
  • 取得元素的大小數值

特別需要注意「取得元素的大小數值」這一項。由於 Reflow 計算耗費較多運算效能,瀏覽器不會立刻執行,而是放到內部等待隊列,每一 frame 批次執行並清空隊列。

這裡的 frame 即 window.requestAnimationFrame() 的幀數。

由於 Reflow 批次執行,當開發者讀取 scrollTop 等物理屬性時,等待隊列中的樣式修改可能尚未 Reflow 到畫面上。為確保程式取到正確位置,瀏覽器會強制觸發一次 Reflow。

這也是許多網站滑鼠滾輪事件監聽沒寫好,導致整個網站卡頓的原因。

Repaint#

經過了 Reflow 的計算,Repaint 的任務是要把計算結果轉換成螢幕上的實際像素顯示。

相比於 Reflow,Repaint 就單純多了,任何可見元素的樣式變更,最後都必然需要重新繪製到畫面上,這是難以避免的效能開銷。

更詳細的 CSS 屬性觸發對照表,可以參考 CSS Triggers。

拯救你的網頁效能#

瀏覽器開發者也知道這些渲染步驟很吃效能,內建的最佳化規則包括:

  • Reflow 極耗效能,瀏覽器會自動批次執行
  • 樣式修改時,瀏覽器會依據屬性自動省略不需要的步驟
    • 修改 width ➡️ Reflow ➡️ Repaint
    • 修改 color ➡️ Repaint

理解上述規則後,常見的最佳化方法如下:

屬性替換#

將物理屬性的變化,換成相似的其他屬性變化,來節省 Reflow 的效能開銷:

  • translate 取代 top 等定位屬性
  • 由於表格的物理屬性會互相影響,容易改一格就整張表 Reflow,可以的話請不要用 table 排版

批次修改#

當需要用 JavaScript 修改樣式時,盡量讓樣式能批次生效:

  • 替換 class name 或修改 cssText,而不是逐個設定 style 屬性
  • 透過 el.cloneNode() 複製一份 DOM,在上面修改樣式後,再替換原本的 DOM
  • 透過 document.createDocumentFragment() 建立 Document Fragments,編輯 DOM 後再加回主 DOM Tree 中

減少影響範圍#

如果 Reflow 是避免不了的,那就只能減少影響範圍了:

  • 盡量避免 DOM、CSSOM Tree 的層級過深,加快 Reflow 的計算
  • 程式取得元素物理屬性時,將結果暫存起來,不要重複觸發計算
  • 改動頻繁的地方建立單獨的圖層(堆疊環境,Stacking Context)

結語#

撰寫 CSS 時,記得考慮對畫面渲染效能的影響。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10217427