CSS @property のパフォーマンスのベンチマーク

公開日: 2024 年 10 月 2 日

新しい CSS 機能を使い始める際は、ウェブサイトのパフォーマンスに与える影響(プラスまたはマイナス)を理解することが重要です。@property がベースラインに追加されました。この投稿では、パフォーマンスへの影響と、悪影響を防ぐための方法について説明します。

PerfTestRunner を使用した CSS のパフォーマンスのベンチマーク

CSS のパフォーマンスをベンチマークするために、「CSS セレクタ ベンチマーク」テストスイートを作成しました。これは Chromium の PerfTestRunner をベースにしており、CSS のパフォーマンスへの影響をベンチマークします。この PerfTestRunner は、Chromium の基盤となるレンダリング エンジンである Blink が内部パフォーマンス テストに使用するものです。

ランナーには、テストに使用する measureRunsPerSecond メソッドが含まれています。1 秒あたりの実行回数が多いほど効果的です。このライブラリを使用した基本的な 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 ベンチマークの一部です。1, 000 個の要素のツリーを構築します。各要素には、ネストされた子要素がいくつかあります。

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 ツリーが変更されないため、このツリー生成は、どのベンチマークも実行される前に 1 回だけ実行されます。

ベンチマークの実行

テストスイートの一部であるベンチマークを実行するには、まずウェブサーバーを起動する必要があります。

npm run start

開始したら、公開された URL でベンチマークにアクセスし、window.startTest() を手動で実行できます。

これらのベンチマークを拡張機能やその他の要因が介在することなく個別に実行するには、PuppeteerCLI からトリガーして、渡されたベンチマークを読み込んで実行します。

これらの @property ベンチマークでは、URL http://localhost:3000/benchmarks/at-rule/at-property.html関連ページにアクセスするのではなく、CLI で次のコマンドを呼び出します。

npm run benchmark at-rule/at-property

これにより、Puppeteer を介してページが読み込まれ、自動的に window.startTest() が呼び出され、結果が報告されます。

CSS プロパティのパフォーマンスのベンチマーク

CSS プロパティのパフォーマンスをベンチマークするには、スタイルの無効化と、ブラウザが行う必要があるその後のスタイルの再計算タスクをどれだけ速く処理できるかをベンチマークします。

スタイルの無効化とは、DOM の変更に応じてスタイルの再計算が必要な要素をマークするプロセスです。最も簡単な方法は、すべての変更に応じてすべてを無効にすることです。

ただし、継承する CSS プロパティと継承しない CSS プロパティを区別する必要があります。

  • ターゲット要素を継承する CSS プロパティが変更された場合、ターゲット要素の下位にあるサブツリー内のすべての要素のスタイルも変更する必要がある場合があります。
  • 継承しない CSS プロパティがターゲット要素で変更されると、その個々の要素のスタイルのみが無効になります。

継承するプロパティと継承しないプロパティを比較することは公平ではありません。そのため、次の 2 セットのベンチマークを実行できます。

  • 継承するプロパティを含む一連のベンチマーク。
  • 継承されないプロパティを含む一連のベンチマーク。

ベンチマーク対象のプロパティは慎重に選択することが重要です。一部のプロパティ(accent-color など)はスタイルのみを無効にしますが、レイアウトやペイントなど、他の要素も無効にするプロパティ(writing-mode など)も多数あります。スタイルのみを無効にするプロパティが必要な場合。

これを判断するには、Blink の CSS プロパティのリストで調べてください。各プロパティには、無効になる内容を一覧表示する invalidate フィールドがあります。

また、そのようなプロパティをベンチマークすると結果が歪むため、リストから independent としてマークされていないプロパティを選択することも重要です。独立したプロパティは、他のプロパティやフラグに副作用を及ぼしません。独立したプロパティのみが変更された場合、Blink は子孫のスタイルを複製し、そのクローンのコピーの新しい値を更新する高速コードパスを使用します。この方法は、完全な再計算よりも高速です。

CSS プロパティを継承するパフォーマンスのベンチマーク

最初のベンチマーク セットは、継承する CSS プロパティに焦点を当てています。テストと比較のために継承するプロパティには、次の 3 種類があります。

  • 継承する通常のプロパティ: accent-color
  • 未登録のカスタム プロパティ: --unregistered
  • inherits: true に登録されているカスタム プロパティ: --registered

未登録のカスタム プロパティはデフォルトで継承されるため、このリストに追加されます。

前述のように、継承するプロパティは、スタイルのみを無効にし、independent としてマークされていないプロパティになるように慎重に選択されています。

登録済みのカスタム プロパティについては、inherits 記述子が true に設定されているプロパティのみがこの実行でテストされます。inherits 記述子は、プロパティが子に継承されるかどうかを決定します。このプロパティが CSS @property で登録されているか、JavaScript CSS.registerProperty で登録されているかは関係ありません。登録自体はベンチマークの対象外です。

ベンチマーク

前述のとおり、ベンチマークを含むページでは、まず DOM ツリーを構築して、変更の影響を確認するのに十分な数のノードをページに含めます。

各ベンチマークはプロパティの値を変更し、その後スタイルの無効化をトリガーします。ベンチマークでは、基本的に、無効になったスタイルをすべて再評価するために、ページの次回の再計算にかかる時間を測定します。

ベンチマークが 1 回実行されると、挿入されたスタイルはリセットされ、次のベンチマークを開始できます。

たとえば、--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 は空になります。

結果

16GB の RAM を搭載した 2021 MacBook Pro(Apple M1 Pro)で 20 回のベンチマークを実行すると、平均は次のようになります。

  • 継承する通常のプロパティ(accent-color): 163 回 / 秒(= 1 回あたり 6.13 ミリ秒)
  • 未登録のカスタム プロパティ(--unregistered): 256 回/秒(= 実行あたり 3.90 ミリ秒)
  • inherits: true で登録されたカスタム プロパティ(--registered): 252 回/秒(= 1 回あたり 3.96 ms)

複数回実行しても、ベンチマークの結果は類似しています。

継承するプロパティの結果を示す棒グラフ。数値が大きいほど、処理速度が速くなります。
図: 継承するプロパティの結果を示す棒グラフ。数値が大きいほど処理速度が速くなります。

結果は、カスタム プロパティを登録した場合の費用は、カスタム プロパティを登録しなかった場合と比較して非常に低いことを示しています。継承する登録済みカスタム プロパティは、未登録のカスタム プロパティの 98% の速度で実行されます。絶対数で見ると、カスタム プロパティを登録すると 0.06 ms のオーバーヘッドが発生します。

継承しない CSS プロパティのパフォーマンスのベンチマーク

次にベンチマークするプロパティは、継承されないプロパティです。ベンチマークに使用できるプロパティは次の 2 種類のみです。

  • 継承しない通常のプロパティ: z-index
  • inherits: false で登録されたカスタム プロパティ: --registered-no-inherit

登録されていないカスタム プロパティは、常に継承されるため、このベンチマークの対象になりません。

ベンチマーク

ベンチマークは、前のシナリオと非常によく似ています。--registered-no-inherit を使用したテストでは、次のプロパティ登録がベンチマークの bootstrap フェーズに挿入されます。

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

結果

16 GB の RAM を搭載した 2021 年モデルの MacBook Pro(Apple M1 Pro)で、これらのベンチマークを 20 回反復実行すると、次の平均値が得られます。

  • 継承しない通常のプロパティ: 290,269 回 / 秒(= 1 回あたり 3.44 µs)
  • 継承しない登録済みカスタム プロパティ: 214,110 回 / 秒(= 1 回あたり 4.67 µs)

テストを複数回にわたって繰り返し実行したところ、これが典型的な結果でした。

継承しないプロパティの結果を示す棒グラフ。数値が大きいほど、処理速度が速くなります。
図: 継承しないプロパティの結果を示す棒グラフ。数値が大きいほど処理速度が速くなります。

ここで注目すべき点は、継承しないプロパティは、継承するプロパティよりもはるかに高速に動作することです。これは通常のプロパティでは想定されることですが、カスタム プロパティにも当てはまります。

  • 通常のプロパティでは、実行回数が 1 秒あたり 163 回から 1 秒あたり 29 万回以上に増加し、パフォーマンスが 1,780% 向上しました。
  • カスタム プロパティの場合、実行回数が 1 秒あたり 252 回から 1 秒あたり 21 万回以上に増加し、パフォーマンスが 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")}
  }`);
},

このベンチマークの 1 秒あたりの実行回数は、「継承しない登録済みカスタム プロパティ」の結果と非常に類似しています(1 秒あたり 214,110 回と 213,158 回)が、注目すべき点はそこではありません。1 つのカスタム プロパティを変更しても、他のプロパティからの登録には影響しないことが期待されます。

このテストの興味深い部分は、登録自体の影響を測定することである。DevTools で確認すると、25,000 件のカスタム プロパティ登録で、最初のスタイル再計算コストが 30ms を少し超えていることがわかります。これが完了すると、これらの登録が存在しても何の影響もありません。

25,000 のカスタム プロパティの登録を行うための費用「スタイルを再計算」がハイライト表示された DevTools のスクリーンショット。ツールチップには、32.42 ms かかったことが示されます。
図: 25,000 件のカスタム プロパティ登録の「スタイルの再計算」費用がハイライト表示された DevTools のスクリーンショット。ツールチップには、32.42ms かかったことが示されます。

結論とまとめ

まとめると、次の 3 つのポイントがあります。

  • @property を使用してカスタム プロパティを登録すると、パフォーマンスが若干低下します。多くの場合、このコストはごくわずかです。カスタム プロパティを登録することで、可能性を最大限に引き出すことができます。これを行わなければ実現することは不可能です。

  • カスタム プロパティを登録するときに inherits: false を使用すると、大きな影響があります。これにより、プロパティが継承されなくなります。したがって、プロパティの値が変更されると、サブツリー全体ではなく、一致した要素のスタイルにのみ影響します。

  • @property の登録が少ない、または多い場合でも、スタイルの再計算には影響しません。登録時にのみ少額の費用がかかりますが、登録が完了すれば費用はかかりません。