このシリーズでは実践をとおして、ウェブパフォーマンスの測定からボトルネックの特定、さらには、その改善方法などを紹介していきます。
先日公開したプロジェクト「FEATGRAPH」では、DOM のサイズが大きく膨れ上がり、パフォーマンス上の問題が表面化したため、CSS に関するパフォーマンス改善を実施しました。
今回は、このプロジェクトを題材として、CSS セレクタのパフォーマンス改善を見ていきます。
パフォーマンスの測定
見出し「パフォーマンスの測定」パフォーマンスの測定には Google Chrome のデベロッパーツールを使用します。その際、ブラウザの拡張機能などの影響を避けるために、対象ページをシークレットモードで開きます。
デベロッパーツールを表示して、1 「Performance(パフォーマンス)」パネルを開き、2 右上の歯車の形をしたアイコンを選択して、キャプチャの設定を開きます。

スロットリングの設定は、3 CPU を「Mid-tier mobile(ミッドティア モバイル)」、ネットワークを「Fast 4G(高速 4G)」に指定しました1。
今回は、CSS セレクタのパフォーマンスを測定するのが主要な目的のため、4 「Enable CSS selector stats(CSS セレクタの統計情報を有効にする)」のチェックをオンにします。
「Enable CSS selector stats」のチェックをオンにすると、その分のオーバーヘッドが測定結果にも影響するので、基本はオフにしておいたほうがよいでしょう。具体的には、後述する「Selector stats のオーバーヘッド」で取り上げます。
この状態で、5 「Record and reload(記録して再読み込み)」を選択して測定を開始しますが、INP(Interaction to Next Paint)も計測したいので、ページが描画された直後に、最初に出現する図形要素をクリックします。

測定結果
見出し「測定結果」それでは記録した内容をもとに、どこにボトルネックがあるかを診断していきます。
Insights
見出し「Insights」まずは、Core Web Vitals のスコアと、パフォーマンス上のインサイト(洞察)を確認します。

1 サイドバーを開いて「Insights」を表示すると、2 INP のスコアが 813 ms であり2、ユーザの操作に対する応答性が極めて低く、改善が必要であることがわかります。
さらに、3 「INP by phase(フェーズ別の INP)」を開いて、INP のサブパートを確認すると、「Presentation delay(表示の遅延)」に問題があると認識できます。
インサイトのなかから「Optimize DOM Size(DOM サイズを最適化する)」を確認すると、DOM の要素数が 6945 であることがわかりますが3、今回のケースでは DOM サイズをこれ以上削減できないため、別の観点から改善ポイントを探っていきます。

Bottom-up
見出し「Bottom-up」続いて、「Bottom-up(ボトムアップ)」タブに切り替えて、1 「Group by category(カテゴリ別に分類)」を選択します。
ここから、2 「Rendering(レンダリング)」カテゴリが全体の 94.3% を占めていることがわかり、そのなかでも、「Recalculate style(スタイルの再計算)」に 86.7% の割合で時間を消費しており、この部分に改善ポイントがあると考えられます。

Selector stats
見出し「Selector stats」CSS セレクタのパフォーマンスを確認するために、「Selector stats」タブを開きます。

統計データの表の項目は以下のとおりです。
| 項目 | 内容 |
|---|---|
| Elapsed (ms) | セレクタの照合に費やした時間 |
| Match Attempts | セレクタの照合を試みた要素の数 |
| Match Count | セレクタと一致した要素の数 |
| % of slow-path non-matches | セレクタと一致しなかった要素と、照合を試みた要素の比率 |
| Selector | セレクタ |
| Style Sheet | スタイルシート |
ここでは、「Selector(セレクタ)」の列に目を向けてみましょう。
Astro の属性([data-astro-cid-*])が含まれているのでわかりづらいですが、「Elapsed(経過時間)」の上位を占めているのは、:has() 擬似クラスを使用しているセレクタです。どうやら、このセレクタ指定がパフォーマンスのボトルネックの一因だと考えられそうです。
コードの調整
見出し「コードの調整」測定した対象ページでは、設定の変更によって図形にフィルタを適用したり、要素の表示・非表示を切り替えているのですが、このインタラクションを :has() 擬似クラスで実装しています。

たとえば、「SHRED」のチェックをオンにしているときに、図形にシュレッダー効果を与えるスタイルは、以下のようなコードで実現しています。
feat-graph:has([value="shred"]:checked) .feat-graph-shape-item:nth-child(1) {
mask-image: linear-gradient(to right, transparent 50%, black 50%);
mask-size: 2% 100%;
} このインタラクションを、:has() ではなく JS で制御する方法に変更していきます。
JS のコードは割愛しますが、チェックボックスやラジオボタンを選択したら、コンテナ要素にカスタムデータ属性(data-*)が追加されるようにして、スタイル指定は以下のように変更します。
feat-graph[data-feat-style*="shred"] .feat-graph-shape-item:nth-child(1) {
mask-image: linear-gradient(to right, transparent 50%, black 50%);
mask-size: 2% 100%;
} 同様の変更を、対象となるチェックボックスやラジオボタンのすべてに反映したうえで、改めてパフォーマンスを測定します。
調整後の測定
見出し「調整後の測定」前述の「パフォーマンスの測定」と同じ方法で記録した結果を見ていきます。
Bottom-up
見出し「Bottom-up」「Bottom-up」を確認すると、依然として「Recalculate style」の相対的な割合は高いですが、処理にかかる時間を 3,240.4 ms から 728.2 ms へと大幅に削減できました。
- Before 調整前の Bottom-up
-

- After 調整後の Bottom-up
-

Selector stats
見出し「Selector stats」「Selector stats」を確認すると、経過時間が大幅に短くなり、:has() から [data-*] に置き換えたことで、これらのセレクタは上位から姿を消しました。
- Before 調整前の Selector stats
-

- After 調整後の Selector stats
-

Insights
見出し「Insights」「Insights」を確認すると、INP は 75 ms になり、良好な範囲(200 ms 以下)まで改善しました。「INP by phase」を開くと、「Presentation delay」が大幅に減少したことがわかります。
- Before 調整前の Insights
-

- After 調整後の Insights
-

以上の結果より、CSS のセレクタ指定を見直したことで、全体的にパフォーマンスが改善したことを確認できました。
Selector stats のオーバーヘッド
見出し「Selector stats のオーバーヘッド」ここまで、CSS セレクタのパフォーマンス改善を見てきましたが、「Selector stats」には、落とし穴とでも呼べるような注意点があります。
キャプチャの設定では「Enable CSS selector stats (slow)」とあるので、この項目を有効にすることで、パフォーマンス測定の処理が遅くなる(slow)と理解できます。
しかし、遅くなるのは記録時のブラウザ側の処理だけではなく、測定結果にも影響を及ぼします。「Selector stats」のチェックを外した状態の測定結果を見てみましょう。
先ほどは、INP が 813 ms から 75 ms へと大幅に改善していましたが、「Selector stats」をオフにした状態では 204 ms から 72 ms へと、改善幅は縮小しました。
- Before 調整前の Insights(Selector stats をオフ)
-

- After 調整後の Insights(Selector stats をオフ)
-

このように、「Selector stats」をオンにした状態では、測定時にオーバーヘッドが上乗せされるため、CSS セレクタのパフォーマンスを調査する目的のとき以外はオフにするのがよさそうです。
おわりに
見出し「おわりに」この記事では、Google Chrome のデベロッパーツールを使用した、CSS セレクタのパフォーマンス改善について説明しました。
通常の使用には、:has() による問題が生じることは少ないでしょう。しかし、DOM サイズが極端に大きいと、パフォーマンスへの影響が顕著になることが確認できました。
まずは、DOM サイズの削減を最優先で検討すべきですが、それが難しい場合には、:has() を置き換えることでパフォーマンスを改善できるか試みる価値はあるでしょう。
ただ、Selector stats のオーバーヘッドを踏まえると、改善の効果が評価しづらいことが現時点における課題ではあります。
参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
- Analyze CSS selector performance during Recalculate Style events | Chrome for Developers(外部リンクを開く)
- How large DOM sizes affect interactivity, and what you can do about it | web.dev(外部リンクを開く)
- Optimize Interaction to Next Paint | web.dev(外部リンクを開く)
- The truth about CSS selector performance | Microsoft Edge Blog(外部リンクを開く)
- I wasted a day on CSS selector performance to make a website load 2ms faster | Trys Mudford(外部リンクを開く)