基準測試 CSS @property 的效能

發布日期:2024 年 10 月 2 日

開始使用新的 CSS 功能時,請務必瞭解這項功能對網站成效的影響 (無論正面或負面皆是)。@property 現已納入基準,本文將探討其對成效的影響,以及您可以採取哪些措施來避免負面影響。

使用「PerfTestRunner」基準化 CSS 成效

為評估 CSS 的效能,我們建構了「CSS 選取器基準測試」測試套件。這個工具採用 Chromium 的 PerfTestRunner,並且會對 CSS 效能影響進行基準測試。這個 PerfTestRunner 是 Blink (Chromium 的基礎轉譯引擎) 用於內部效能測試的項目。

執行器包含用於測試的 measureRunsPerSecond 方法。每秒執行次數越多越好。使用此程式庫的基本 measureRunsPerSecond 基準測試如下所示:

const testResults = PerfTestRunner.measureRunsPerSecond({
  "Test Description",
  iterationCount: 5,
  bootstrap: function() {
    // Code to execute before all iterations run
    // For example, you can inject a style sheet here
  },
  setup: function() {
    // Code to execute before a single iteration
  },
  run: function() {
    // The actual test that gets run and measured.
    // A typical test adjusts something on the page causing a style or layout invalidation
  },
  tearDown: function() {
    // Code to execute after a single iteration has finished
    // For example, undo DOM adjustments made within run()
  },
  done: function() {
    // Code to be run after all iterations have finished.
    // For example, remove the style sheets that were injected in the bootstrap phase
  },
});

measureRunsPerSecond 的每個選項都會透過程式碼區塊中的註解加以說明,其中 run 函式是需要評估的核心部分。

CSS 選取器基準需要 DOM 樹狀結構

由於 CSS 選取器的效能也取決於 DOM 的大小,因此這些基準測試需要適當大小的 DOM 樹狀結構。系統會自動產生這個 DOM 樹狀結構,而非手動建立。

舉例來說,下列 makeTree 函式是 @property 基準測試的一部分。它會建構 1000 個元素的樹狀結構,每個元素都包含一些內嵌的子項。

const $container = document.querySelector('#container');

function makeTree(parentEl, numSiblings) {
  for (var i = 0; i <= numSiblings; i++) {
    $container.appendChild(
      createElement('div', {
        className: `tagDiv wrap${i}`,
        innerHTML: `<div class="tagDiv layer1" data-div="layer1">
          <div class="tagDiv layer2">
            <ul class="tagUl">
              <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
            </ul>
          </div>
        </div>`,
      })
    );
  }
}

makeTree($container, 1000);

由於 CSS 選取器基準不會修改 DOM 樹狀結構,因此此樹狀結構產生作業只會在任何基準測試執行前執行一次。

執行基準測試

如要執行測試套件中的基準測試,您必須先啟動網路伺服器:

npm run start

啟動後,您可以前往基準測試的發布網址,手動執行 window.startTest()

為了在不介入任何擴充功能或其他因素的情況下,單獨執行這些基準測試,Puppeteer從 CLI 觸發,以載入並執行傳遞的基準測試。

針對這些 @property 基準測試,請在 CLI 上叫用下列指令,而非造訪其網址 http://localhost:3000/benchmarks/at-rule/at-property.html相關網頁

npm run benchmark at-rule/at-property

這會透過 Puppeteer 載入網頁、自動呼叫 window.startTest(),並回報結果。

基準測試 CSS 屬性的效能

如要基準測試 CSS 屬性的效能,請測試其處理樣式無效化的速度,以及瀏覽器需要執行的後續樣式重新計算作業。

樣式無效化是指標示哪些元素需要重新計算樣式,以回應 DOM 的變更。最簡單的方法是,在每次變更時讓所有內容失效。

在這種情況下,您必須區分要繼承的 CSS 屬性,以及不繼承的 CSS 屬性。

  • 當繼承目標元素變更的 CSS 屬性時,目標元素底下子樹狀結構中所有元素的樣式也需要變更。
  • 當 CSS 屬性未繼承目標元素的變更時,只有該個別元素的樣式會失效。

由於將會比較會繼承的資源與不會繼承的資源,因此我們會執行兩組基準:

  • 一組具有可繼承屬性的基準。
  • 一組基準測試,其中包含不會繼承的屬性。

請務必謹慎選擇要做為基準的資源。雖然某些屬性 (例如 accent-color) 只會使樣式失效,但許多屬性 (例如 writing-mode) 也會使版面配置或繪製等其他項目失效。您需要的屬性只會使樣式失效。

如要判斷這項屬性是否適用於 Blink,請查看 Blink 的 CSS 屬性清單。每個資源都有一個 invalidate 欄位,列出哪些資源會失效。

此外,請務必從清單中選取未標示為 independent 的房源,因為基準測試這類房源會導致結果偏差。獨立屬性不會對其他屬性或標記產生任何附帶影響。如果只有獨立的屬性發生變更,Blink 會使用快速程式碼路徑,複製子項的樣式,並在該複製副本中更新新值。這種方法比完整重新計算更快。

針對沿用的 CSS 資源成效進行基準測試

第一組基準測試著重於繼承的 CSS 屬性。繼承的三種屬性可用於測試及比較:

  • 繼承以下一般資源:accent-color
  • 未註冊的自訂屬性:--unregistered
  • 使用 inherits: true 註冊的自訂資源:--registered

未註冊的自訂屬性會加入此清單,因為這些屬性預設會繼承。

如前所述,繼承了沿用的屬性是經過審慎選擇,因此只有樣式無效,且沒有標示 independent 的屬性才是如此。

至於已註冊的自訂屬性,這次執行作業只會測試 inherits 描述元設為 true 的屬性。inherits 描述元決定屬性是否會向下繼承至子項。無論這個屬性是透過 CSS @property 或 JavaScript CSS.registerProperty 註冊,註冊本身都不屬於基準測試。

基準

如先前所述,包含基準的網頁會先建構 DOM 樹狀結構,讓網頁擁有足夠的節點組合,以便查看變更的影響。

每個基準測試都會變更屬性值,並觸發樣式無效化。基準測試基本上會評估網頁下次重新計算時,重新評估所有無效樣式的時間。

單一基準測試完成後,系統會重設所有插入的樣式,以便開始下一個基準測試。

舉例來說,測量變更 --registered 樣式的效能基準如下所示:

let i = 0;
PerfTestRunner.measureRunsPerSecond({
  description,
  iterationCount: 5,
  bootstrap: () => {
    setCSS(`@property --registered {
      syntax: "<number>";
      initial-value: 0;
      inherits: true;
    }`);
  },
  setup: function() {
    // NO-OP
  },
  run: function() {
    document.documentElement.style.setProperty('--registered', i);
    window.getComputedStyle(document.documentElement).getPropertyValue('--registered'); // Force style recalculation
    i = (i == 0) ? 1 : 0;
  },
  teardown: () => {
    document.documentElement.style.removeProperty('--registered');
  },
  done: (results) => {
    resetCSS();
    resolve(results);
  },
});

測試其他類型資源的基準測試會以相同方式運作,但由於沒有要註冊的資源,因此會產生空白的 bootstrap

成果

在 2021 年 MacBook Pro (Apple M1 Pro) 上執行這些基準測試,並重複 20 次,記憶體為 16 GB,可獲得以下平均值:

  • 繼承的一般屬性 (accent-color):每秒 163 次執行 (= 每次執行 6.13 毫秒)
  • 未註冊的自訂屬性 (--unregistered):每秒 256 次執行 (= 每次執行作業 3.90 毫秒)
  • 使用 inherits: true 註冊自訂資源 (--registered):每秒 252 次執行 (= 每次執行 3.96 毫秒)

在多個執行作業中,基準測試會產生類似的結果。

顯示繼承房源結果的長條圖。數字越大,執行速度越快。
圖:顯示繼承屬性結果的長條圖。數字越大,執行速度越快。

結果顯示,與未註冊自訂資源相比,註冊自訂資源的成本非常低。已註冊的自訂屬性,運作速度是未註冊自訂屬性的 98%。以絕對數字來說,註冊自訂屬性會增加 0.06 毫秒的額外負擔。

基準化未繼承的 CSS 屬性效能

下一個要基準測試的屬性是不會繼承的屬性。以下只有兩種屬性可用於基準化:

  • 未沿用的一般屬性:z-index
  • 已註冊的含有 inherits: false 的自訂屬性:--registered-no-inherit

未註冊的自訂資源無法納入此基準,因為這些資源一律會繼承。

基準

基準測試與先前的情況非常相似。針對使用 --registered-no-inherit 的測試,以下屬性註冊作業會在基準測試的 bootstrap 階段中插入:

@property --registered-no-inherit {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

成果

在 2021 年 MacBook Pro (Apple M1 Pro) 上執行這些基準測試,並重複 20 次,記憶體為 16 GB,可獲得以下平均值:

  • 不會繼承的一般資源:每秒 290,269 次執行 (即每執行 3.44µs)
  • 未繼承的註冊自訂屬性:每秒 214,110 次執行 (即每執行一次 4.67µs)

我們多次重複執行這項測試,並取得以下結果。

顯示不繼承屬性的結果的長條圖。數字越大,執行速度越快。
圖:顯示不繼承屬性的結果的長條圖。數字越大,執行速度越快。

這裡最明顯的差異在於,不繼承的資源比繼承的資源速度快得多。雖然一般資源會預期這種情況,但自訂資源也會發生這種情況。

  • 對於一般資源,執行次數從每秒 163 次提高到每秒 29 萬次以上,效能提升了 1780%!
  • 自訂資源的執行次數從每秒 252 次提高到每秒 214 千次,成效提升 848%!

這項研究的主要結論是,註冊自訂資源時使用 inherits: false 會產生顯著影響。如果您可以使用 inherits: false 註冊自訂屬性,就一定會這麼做。

額外基準:登錄多個自訂資源

另一個值得比較的項目是,註冊大量自訂資源會帶來什麼影響。如要這樣做,請使用 --registered-no-inherit 重新執行測試,並預先執行 25,000 次其他自訂資源註冊作業。這些自訂屬性用於 :root

這些註冊作業會在基準測試的 setup 步驟中完成:

setup: () => {
  const propertyRegistrations = [];
  const declarations = [];

  for (let i = 0; i < 25000; i++) {
    propertyRegistrations.push(`@property --custom-${i} { syntax: "<number>"; initial-value: 0; inherits: true; }`);
    declarations.push(`--custom-${i}: ${Math.random()}`);
  }

  setCSS(`${propertyRegistrations.join("\n")}
  :root {
    ${declarations.join("\n")}
  }`);
},

這個基準測試的每秒執行次數與「未繼承的已註冊自訂屬性」(每秒 214,110 次執行次數,相較於每秒 213,158 次執行次數)的結果非常相似,但這並不是值得關注的部分。畢竟,變更一個自訂資源不會受到其他資源的註冊影響。

這項測試最有趣的部分,就是評估註冊本身的影響。切換至 DevTools,您可以看到 25,000 個自訂屬性註冊項目的初始樣式重新計算成本略高於 30ms。完成後,這些註冊項目就不會再對其他項目造成影響。

開發人員工具螢幕截圖,醒目顯示執行 25,000 項自訂屬性註冊作業的「重新計算樣式」費用。工具提示指出耗時 32.42 毫秒
圖:開發人員工具螢幕截圖,其中標示出註冊 25,000 個自訂資源的「Recalculate Style」成本。工具提示指出耗時 32.42ms

結語和重點

總而言之,我們從這一切中學到三件事:

  • 透過 @property 註冊自訂屬性會產生些許效能。這筆費用通常可以忽略不計,因為註冊自訂資源可發揮其最大潛力,而這項作業必須註冊自訂資源才能達成。

  • 在註冊自訂屬性時使用 inherits: false 有實質影響。否則資源就不會沿用設定。因此,當屬性值變更時,只會影響相符元素的樣式,而不會影響整個子樹狀結構。

  • @property 註冊項目數量多寡不會影響樣式重新計算。註冊時,前期成本只會極低,但作業完成後,您就可以放心了。