このシリーズでは実践をとおして、ウェブパフォーマンスの測定からボトルネックの特定、さらには、その改善方法などを紹介していきます。

先日公開したプロジェクト「FEATGRAPH」では、DOM のサイズが大きく膨れ上がり、パフォーマンス上の問題が表面化したため、CSS に関するパフォーマンス改善を実施しました。

今回は、このプロジェクトを題材として、CSS セレクタのパフォーマンス改善を見ていきます。

パフォーマンスの測定

見出し「パフォーマンスの測定」

パフォーマンスの測定には Google Chrome のデベロッパーツールを使用します。その際、ブラウザの拡張機能などの影響を避けるために、対象ページをシークレットモードで開きます。

デベロッパーツールを表示して、1 「Performance(パフォーマンス)」パネルを開き、2 右上の歯車の形をしたアイコンを選択して、キャプチャの設定を開きます。

デベロッパーツールのスクリーンショット
「Performance」のキャプチャの設定を開いた状態

スロットリングの設定は、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)も計測したいので、ページが描画された直後に、最初に出現する図形要素をクリックします。

「FEATGRAPH」のスクリーンショット
INP を測定するため、記録中に図形要素をクリックして詳細情報を表示

それでは記録した内容をもとに、どこにボトルネックがあるかを診断していきます。

まずは、Core Web Vitals のスコアと、パフォーマンス上のインサイト(洞察)を確認します。

「Insights」のスクリーンショット

1 サイドバーを開いて「Insights」を表示すると、2 INP のスコアが 813 ms であり2、ユーザの操作に対する応答性が極めて低く、改善が必要であることがわかります。

さらに、3 「INP by phase(フェーズ別の INP)」を開いて、INP のサブパートを確認すると、「Presentation delay(表示の遅延)」に問題があると認識できます。

インサイトのなかから「Optimize DOM Size(DOM サイズを最適化する)」を確認すると、DOM の要素数が 6945 であることがわかりますが3、今回のケースでは DOM サイズをこれ以上削減できないため、別の観点から改善ポイントを探っていきます。

「Optimize DOM Size」のスクリーンショット
過大な DOM サイズは応答性やメモリの消費に悪影響を及ぼす

続いて、「Bottom-up(ボトムアップ)」タブに切り替えて、1 「Group by category(カテゴリ別に分類)」を選択します。

ここから、2 「Rendering(レンダリング)」カテゴリが全体の 94.3% を占めていることがわかり、そのなかでも、「Recalculate style(スタイルの再計算)」に 86.7% の割合で時間を消費しており、この部分に改善ポイントがあると考えられます。

「Bottom-up」のスクリーンショット

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

「Selector stats」のスクリーンショット

統計データの表の項目は以下のとおりです。

項目内容
Elapsed (ms)セレクタの照合に費やした時間
Match Attemptsセレクタの照合を試みた要素の数
Match Countセレクタと一致した要素の数
% of slow-path non-matchesセレクタと一致しなかった要素と、照合を試みた要素の比率
Selectorセレクタ
Style Sheetスタイルシート

ここでは、「Selector(セレクタ)」の列に目を向けてみましょう。

Astro の属性([data-astro-cid-*])が含まれているのでわかりづらいですが、「Elapsed(経過時間)」の上位を占めているのは、:has() 擬似クラスを使用しているセレクタです。どうやら、このセレクタ指定がパフォーマンスのボトルネックの一因だと考えられそうです。

測定した対象ページでは、設定の変更によって図形にフィルタを適用したり、要素の表示・非表示を切り替えているのですが、このインタラクションを :has() 擬似クラスで実装しています。

「FEATGRAPH」のスクリーンショット
「SHRED」のチェックをオンにした状態

たとえば、「SHRED」のチェックをオンにしているときに、図形にシュレッダー効果を与えるスタイルは、以下のようなコードで実現しています。

:has() でオン・オフの切り替えを判定するスタイルの例
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」を確認すると、依然として「Recalculate style」の相対的な割合は高いですが、処理にかかる時間を 3,240.4 ms から 728.2 ms へと大幅に削減できました。

Before 調整前の Bottom-up
「Bottom-up(ボトムアップ)」のスクリーンショット
After 調整後の Bottom-up
「Bottom-up(ボトムアップ)」のスクリーンショット

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

Before 調整前の Selector stats
「Selector stats(セレクタの統計データ)」のスクリーンショット
After 調整後の Selector stats
「Selector stats(セレクタの統計データ)」のスクリーンショット

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

Before 調整前の Insights
「Insights」のスクリーンショット
After 調整後の Insights
「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 をオフ)
「Insights」のスクリーンショット
After 調整後の Insights(Selector stats をオフ)
「Insights」のスクリーンショット

このように、「Selector stats」をオンにした状態では、測定時にオーバーヘッドが上乗せされるため、CSS セレクタのパフォーマンスを調査する目的のとき以外はオフにするのがよさそうです。

この記事では、Google Chrome のデベロッパーツールを使用した、CSS セレクタのパフォーマンス改善について説明しました。

通常の使用には、:has() による問題が生じることは少ないでしょう。しかし、DOM サイズが極端に大きいと、パフォーマンスへの影響が顕著になることが確認できました。

まずは、DOM サイズの削減を最優先で検討すべきですが、それが難しい場合には、:has() を置き換えることでパフォーマンスを改善できるか試みる価値はあるでしょう。

ただ、Selector stats のオーバーヘッドを踏まえると、改善の効果が評価しづらいことが現時点における課題ではあります。

本記事の作成にあたり、以下のウェブページを参考にしました。

脚注

  1. 「Mid-tier mobile(ミッドティア モバイル)」や「Low-tier mobile(ローティア モバイル)」を使用するには、事前にキャリブレートする必要があります。

  2. INP が優良と評価されるためには、インタラクションを開始してから次のフレームが描画されるまでの時間を 200 ms 以下に収める必要があります。

  3. Lighthouse では、DOM ノードが 800 を超えると警告し、1400 を超えるとエラーが表示されます。