公開日: 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()
を手動で実行できます。
これらのベンチマークを拡張機能やその他の要因が介在することなく個別に実行するには、Puppeteer を CLI からトリガーして、渡されたベンチマークを読み込んで実行します。
これらの @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
を少し超えていることがわかります。これが完了すると、これらの登録が存在しても何の影響もありません。
結論とまとめ
まとめると、次の 3 つのポイントがあります。
@property
を使用してカスタム プロパティを登録すると、パフォーマンスが若干低下します。多くの場合、このコストはごくわずかです。カスタム プロパティを登録することで、可能性を最大限に引き出すことができます。これを行わなければ実現することは不可能です。カスタム プロパティを登録するときに
inherits: false
を使用すると、大きな影響があります。これにより、プロパティが継承されなくなります。したがって、プロパティの値が変更されると、サブツリー全体ではなく、一致した要素のスタイルにのみ影響します。@property
の登録が少ない、または多い場合でも、スタイルの再計算には影響しません。登録時にのみ少額の費用がかかりますが、登録が完了すれば費用はかかりません。