避免發生複雜的大型版面配置和版面配置閃爍

版面配置是指瀏覽器判斷元素幾何資訊 (元素在頁面中的大小和位置) 的過程。每個元素都會根據使用的 CSS、元素內容或父項元素,提供明確或隱含的大小資訊。在 Chrome 中,這個程序稱為「版面配置」。

版面配置是瀏覽器用來判斷元素幾何資訊的過程,包括元素的大小和網頁中的位置。每個元素都會根據所使用的 CSS、元素內容或父元素,提供明確或隱含的大小資訊。這項程序稱為 Chrome 中的 Layout (以及 Edge 等衍生瀏覽器),在 Firefox 中則名為 Reflow,但程序大致相同。

與樣式計算方式類似,版面配置成本的立即性考量因素如下:

  1. 需要版面配置的元素數量,這是網頁 DOM 大小的副產品。
  2. 版面配置的複雜度。

摘要

  • 版面配置會直接影響互動延遲時間
  • 版面配置通常會限定在整份文件內。
  • DOM 元素的數量會影響效能,因此請盡可能避免觸發版面配置。
  • 避免強制同步版面配置和版面配置耗損;請先讀取樣式值,再變更樣式。

版面配置對互動延遲的影響

使用者與網頁互動時,這些互動應盡可能快速。互動完成所需的時間 (結束時間為瀏覽器呈現下一個影格,以顯示互動結果) 稱為互動延遲。這是「與下一個顯示的內容互動」指標評估的網頁效能面向。

瀏覽器在回應使用者互動時,呈現下一個影格的時間長度稱為互動的呈現延遲。互動的主要目的是提供視覺回饋,向使用者傳達發生的事件,而視覺更新可能需要進行一些版面配置工作才能達成這個目標。

為了盡可能降低網站的 INP,請盡量避免使用版面配置。如果無法完全避免版面配置,請務必限制版面配置作業,讓瀏覽器能快速顯示下一個影格。

盡量避免使用版面配置

變更樣式時,瀏覽器會檢查是否有任何變更需要計算版面配置,以及是否需要更新該轉譯樹狀結構。如要變更「幾何屬性」,例如寬度、高度、左側或頂端,都需要版面配置。

.box {
  width: 20px;
  height: 20px;
}

/**
  * Changing width and height
  * triggers layout.
  */

.box--expanded {
  width: 200px;
  height: 350px;
}

版面配置的範圍幾乎總是整份文件。如果元素很多,您可能需要花費很長的時間來找出所有元素的位置和尺寸。

如果無法避免版面配置,那麼重點就是再次使用 Chrome 開發人員工具,查看所需時間,並判斷版面配置是否導致瓶頸。首先,請開啟開發人員工具,前往「時間軸」分頁,按下「錄製」並與網站互動。停止錄製後,您會看到網站成效的詳細資料:

開發人員工具在版面配置中顯示的時間過長。

深入研究上述範例中的追蹤記錄,我們發現每個影格在版面配置中耗費的時間超過 28 毫秒,而動畫在畫面上顯示一個影格只需要 16 毫秒,因此這個值過高。您也可以看到,DevTools 會告訴您樹狀結構的大小 (在本例中為 1,618 個元素),以及需要排版的節點數量 (在本例中為 5 個)。

請注意,一般建議是盡可能避免使用版面配置,但有時無法避免使用版面配置。如果無法避免版面配置,請注意版面配置費用與 DOM 大小有關係。雖然這兩者之間並未緊密結合,但較大的 DOM 通常會導致較高的版面配置成本。

避免強制同步版面配置

如何在螢幕上提交影格:

使用 Flexbox 做為版面配置。

首先,JavaScript 會執行「然後」樣式計算,再執行版面配置。不過,您可以使用 JavaScript 強制瀏覽器提早執行版面配置。這就是所謂的強制同步版面配置

首先須留意,由於 JavaScript 會執行前一個影格中的所有舊版面配置值,以供您查詢。舉例來說,如果您想在影格開始時輸出元素的高度 (我們稱之為「box」),可以編寫以下程式碼:

// Schedule our function to run at the start of the frame:
requestAnimationFrame(logBoxHeight);

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

如果您在詢問方塊高度「之前」變更了方塊樣式,會造成問題:

function logBoxHeight () {
  box.classList.add('super-big');

  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);
}

接下來,為了回答高度問題,瀏覽器必須套用樣式變更 (因為已新增 super-big 類別),再執行版面配置。只有在這種情況下,系統才能傳回正確的高度。這項工作不必要且可能耗費不菲。

因此,您應一律先批次讀取樣式 (瀏覽器可使用先前影格版面配置值),再執行任何寫入作業:

正確完成上述函式後,會變成:

function logBoxHeight () {
  // Gets the height of the box in pixels and logs it out:
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

在大多數情況下,您不必套用樣式,然後再查詢值;使用上一個影格中的值就足夠了。以非同步或更早的方式執行樣式計算和版面配置,可能會遇到瓶頸,但您通常不會想要這麼做。

避免版面配置輾轉

有方法讓強制同步版面配置更臻完善,那就是快速連續執行多種版面配置。請查看以下程式碼:

function resizeAllParagraphsToMatchBlockWidth () {
  // Puts the browser into a read-write-read-write cycle.
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = `${box.offsetWidth}px`;
  }
}

這個程式碼會循環處理一組段落,並且設定每個段落的寬度,以符合名為「box」的元素寬度。這看起來似乎沒什麼問題,但問題在於迴圈的每個疊代都會讀取樣式值 (box.offsetWidth),然後立即使用該值更新段落的寬度 (paragraphs[i].style.width)。在迴圈的下一個疊代中,瀏覽器必須考量到自上次要求 offsetWidth (在前一個疊代中) 以來,樣式已有所變更,因此必須套用樣式變更並執行版面配置。這種情況會在每次疊代時發生。

這個範例的修正方式是再次讀取,然後寫入值:

// Read.
const width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth () {
  for (let i = 0; i < paragraphs.length; i++) {
    // Now write.
    paragraphs[i].style.width = `${width}px`;
  }
}

如要確保安全性,建議您使用 FastDOM,這項功能會自動為您批次處理讀取和寫入作業,並避免您意外觸發強制同步版面配置或版面配置耗損。