如何優化像素管道的 Styles 和 Layout?

更詳細探討如何優化像素管道(Browser Rendering Pipeline)的樣式計算(Recalculate Styles)和版面配置(Recaculate Layout / Reflow)這兩個階段。

Browser Rendering Pipeline

圖片來源:Rendering Performance

簡化 Selector Matching

Selector Matching 是比對哪些樣式規則應該要應用到哪些 DOM Element 的過程。

查找的範例如下。

<div class="box"></div>
<div class="box"></div>
<div class="box b-3"></div>
.box: nth-child(3) .box .b-3;

若樣式規則只是要找一個元素還好,而樣式變更的複雜度是隨著元素個數而線性成長的,若有 10 個要改的元素,成本就是多 10 倍。舉例來說,在查找一個元素的情況下,.box:nth-child(3) 的花費假設是 2ms,.box .b-3 假設是 1ms,若要查找 100 個元素,.box:nth-child(3) 花費 200ms,.box .b-3 花費 100ms,足足多了一倍。這是由於愈複雜的樣式規則,需要在 Render Tree 來回查找的時間就更多。

解法是使用 BEM,或類似的 CSS Class 命名設計模式。BEM 的命名方式順道簡化了樣式規則,並且由於 Class Matching 是現今瀏覽器最快的比對方法,因此 BEM 不僅顧及模組化、重用性與可讀性,還兼顧改善效能。

所以,上面的範例可改寫為

.box--three

不然就是思考怎麼減少 DOM Element 了。

Forced Synchronous Layout

強制同步版面配置(Forced Synchronous Layout,簡稱 FSL)發生的原因是由於在 JavaScript 程式碼中觸發 Layout 階段的 CSS 指令,例如:讀取某個元素的 offsetWidth 值,就會強迫瀏覽器在此幀(Frame)就必須更新,導致瀏覽器馬上計算樣式和配置版面,接著更新樣式,使得瀏覽器進入讀取/寫入的循環。

Forced Synchronous Layout

圖片來源:Browser Rendering Optimization: Styles and Layout

看文字描述還真不好理解,來看範例吧。

以下分別有 A、B、C 三個範例,哪一個不會造成 Forced Synchronous Layout?

(A)

divs.forEach(function (elem, index, arr) {
  if (window.scrollY < 200) {
    element.style.opacity = 0.5;
  }
});

(B)

divs.forEach(function (elem, index, arr) {
  if (elem.offsetHeight < 500) {
    elem.style.maxHeight = '100vh';
  }
});

(C)

var newWidth = container.offsetWidth;

divs.forEach(function (elem, index, arr) {
  element.style.width = newWidth;
});

防雷-第一次… ヽ(∀゚ )人(゚∀゚)人( ゚∀)人(∀゚ )人(゚∀゚)人( ゚∀)ノ

防雷-第二次… ヽ(∀゚ )人(゚∀゚)人( ゚∀)人(∀゚ )人(゚∀゚)人( ゚∀)ノ

防雷-最後提醒… ヽ(∀゚ )人(゚∀゚)人( ゚∀)人(∀゚ )人(゚∀゚)人( ゚∀)ノ

答案是(C)。

(A) 讀取 window.scrollY 會造成 Layout,接著設定透明度,這過程導致瀏覽器進入讀取/寫入循環,進而不停 Forced Synchronous Layout,會造成 Layout Thrashing。

修正如下,window.scrollY 使用前一幀的值即可,不需要在此幀立刻取得最新更新的值。

const positionY = window.scrollY;

divs.forEach(function (elem, index, arr) {
  if (positionY < 200) {
    element.style.opacity = 0.5;
  }
});

(B) 讀取 elem.offsetHeight 會造成 Layout,接著修改樣式,這過程導致瀏覽器進入讀取/寫入循環,進而不停 Forced Synchronous Layout,會造成 Layout Thrashing。

修正如下,先讀取樣式屬性值,然後批次更新樣式。這意味著使用前幀所取得的資料,接著再一次更新完所有的樣式修改。因此,Browser Rendering Pipeline 的「JavaScript/Styles -> Layout -> Paint -> Composite」只會順暢地走過一次。

if (elem.offsetHeight < 500) {
  // 先讀取樣式屬性值,
  divs.forEach(function (elem, index, arr) {
    // 然後批次更新樣式
    elem.style.maxHeight = '100vh';
  });
}

(C) 將讀取 container.offsetWidth 放在迴圈之外,因此 Layout 只會執行一次,接著瀏覽器會批次更新 element 樣式,不會造成 Forced Synchronous Layout 或 Layout Thrashing。

Layout Thrashing

若不斷地 Forced Synchronous Layout,就會導致 Layout Thrashing。

範例如下,點擊「Click Me」後,將藍色區塊的寬度,設定與綠色區塊的寬度相同。

Layout Thrashing Demo

這部分的程式碼是這樣寫的…先讀取 greenBlock 目前的寬,然後再將這個寬的數值,設定給每一個 paragraphs。

const paragraphs = document.querySelectorAll('p');
const clickme = document.getElementById('clickme');
const greenBlock = document.getElementById('block');

clickme.onclick = function(){
  greenBlock.style.width = '600px';

  for (let p = 0; p < paragraphs.length; p++) {
    let blockWidth = greenBlock.offsetWidth;
    paragraphs[p].style.width = `${blockWidth}px`;
  }
};

程式碼與 Demo,或點此下載。

這樣的程式碼導致 Layout Thrashing,從 Chrome DevTools 會在右上角用紅色三角形標記這個 Style Calculations 和 Layout 階段。

造成這個問題的原因是由於在迴圈中讀取 greenBlock.offsetWidth 的值,導致瀏覽器必須不斷做樣式計算和版面配置才能提供接下來 paragraphs[p].style.width 所需的值,說白話就是強迫在此幀取得更新值,並做樣式更新,一直打斷重來的概念。

Forced Reflow is a likely performance bottleneck

超多紅色警示的,大概有兩百多個 XD

這密密麻麻的紅色警示就是不斷的 Style Calculations -> Layout,下圖放大近看。

Layout Thrashing

Details 上會看到警示訊息「Forced reflow is a likely performance bottleneck.」。

Forced Reflow is a likely performance bottleneck.

解法是不要在迴圈裡面讀取 greenBlock.offsetWidth 的值,而是放到 onclick 的 callback 裡存成常數,迴圈讀取這個常數即可,就可避免迫使瀏覽器不斷重新樣式計算和版面配置,改為一次讀取、批次更新。

clickme.onclick = function () {
  greenBlock.style.width = '600px';
  const blockWidth = greenBlock.offsetWidth;

  for (let p = 0; p < paragraphs.length; p++) {
    paragraphs[p].style.width = `${blockWidth}px`;
  }
};

改善後,在 Chrome DevTools 可看到做完 Style Calculations 後,Layout 是一併批次處理完成的,也不再看到警示訊息,可點此下載。

改善 Layout Thrashing

總結

總會有人問「使用 JavaScript 或 CSS 實作動畫,哪一個效能比較好?」,但實際上應從檢視像素管道(Browser Rendering Pipeline)著手,使用 width 或 position 的成本是比較高的,無論使用 JavaScript 或 CSS 都會導致較差的效能;在錯誤時機使用 JavaScript 可能造成強制同步版面配置(Forced Synchronous Layout)或快速連續版面配置(Layout Thrashing)也會導致效能不佳。

參考資料


Layout Thrashing Forced Synchronous Layout 關鍵轉譯路徑 效能調校 轉譯效能 Rendering Performance Critical Rendering Path BEM Chrome DevTools css css3 animations 前端效能 系列文

Summer Tang
Sponsored
Summer Tang 一對一諮詢
Summer|TSMC 主任工程師|擁有 15 年資深 Web 架構經驗,職涯歷經跨國頂尖企業與新創。專精於高流量企業級應用開發,核心技術涵蓋微前端、效能優化、測試自動化與 SEO。曾出版多本技術著作,並多次擔任 MOPCON、WebConf 等大型技術年會講者。
了解更多 →