【実務・中級編】 機能擬似クラスのネストとパフォーマンス – CSS実践ガイド

やあ、よく来てくれた。
今日も今日とて、巨大化していくCSSの森で迷子になっていないかい? 「なんかこのコンポーネント、修正するたびに他のところまで崩れるんだけど……」「セレクタの書き方がカオスすぎて、誰が書いたか分からない……」なんて、現場のチャットで頭を抱えている姿が目に浮かぶようだ。

中級からもう一歩上のシニアの領域に上がろうとしている君なら、`(:is()や :where()といった「機能擬似クラス」)の便利さはもう骨身に染みていることだろう。これらのおかげで、長ったらしいセレクタの重複を書かなくて済むようになったよね。

だがな、ここで一つ問いかけたい。
「便利だからといって、`:is()`や`:where()`を無思考に、あるいは面白半分にネストさせてはいないか?」

今回は、この機能擬似クラスのネストがブラウザの裏側でどう評価され、レンダリングのパフォーマンスにどんな爪痕を残すのか、その泥臭い真実を解き明かしていこう。現場で即戦力になる知見を叩き込むから、しっかりついてきてくれ。

—

1. そもそもお前らは中で何をやっているのか:` :is() ` と ` :where() ` のおさらい

まず前提を揃えよう。` :is() `(マッチング擬似クラス)と ` :where() `(特定性ゼロの擬似クラス)は、CSSの記述量を劇的に減らす救世主だ。

例えば、これまでならこう書いていた地獄のようなセレクタ:

/ 昔ながらの冗長な書き方:保守性が最悪 /
main article h1,
main article h2,
main article h3 {
color: #333;
}

これがこうスッキリ書けるようになる。

/ :is() を使ったモダンな書き方 /
main article :is(h1, h2, h3) {
color: #333;
}

最高だよな。見た目もスマートだし、DRY原則にも忠実だ。
だが、問題はこの「糖衣構文(シンタックスシュガー)」の皮を剥いだとき、ブラウザが裏側でどういう計算をしているかだ。

特定性(Specificity)の決定的な違い

  • `:is()` の特定性: 引数の中で最も強い特定性を持つセレクタのものになる。
  • `:where()` の特定性: 常に `0` だ。どれだけ複雑なセレクタを突っ込んでも、特定性は微動だにしない。

この仕様の差が、ネストさせたときに牙を向くんだ。

—

2. ネストの魔力:ブラウザは裏側でどう評価しているのか?

さて、本題の「ネスト」だ。
「` :is() ` の中にさらに ` :is() ` を入れたら、もっとスマートになるんじゃね?」――そう思ったことはないか?

例えば、こんなコードを書いたとする。

/ 危険なネストの例 /
:is(header, footer) :is(.nav, .sidebar) :is(a, button) {
cursor: pointer;
}

これをブラウザのレンダリングエンジン(BlinkやGeckoなど)は、どう解釈していると思う?
人間から見れば「ヘッダーかフッターの中にある、ナビかサイドバーの中にある、リンクかボタン」という美しい階層構造に見える。

だが、ブラウザのセレクタマッチング(多くの場合、DOMツリーを右から左へ、つまり右端のキーセレクタから逆向きに走査する)において、ネストされた機能擬似クラスは、次のような「コンビネーションの爆発」を引き起こす。

逆向きマッチングの悲劇

ブラウザはまず右端の `:is(a, button)` を見つける。そして親方向へ遡る際、`:is()` の中身がさらに別の `:is()` で包まれていると、評価すべきパス(組み合わせ)が幾何級数的に増える。

単なる1段の `:is()` ならまだ可愛いものだが、これを深くネストさせたり、` :where() ` と複雑に組み合わせたりすると、ブラウザのスタイル計算機(Style Engine)に無駄なCPUサイクルを消費させることになる。

特に大規模なSPA(Single Page Application)で、数万個のDOM要素がうごめく中、このような「意図の見えない深すぎる擬似クラスのネスト」が散在していると、インタラクション時のリフロー・リペイントのパフォーマンスが確実に劣化する。ユーザーのスクロールがカクつく原因の多くは、こういうところにあるんだ。

—

3. 現場で使える!パフォーマンスを意識した「きれいなコード」の書き方

じゃあ、どう書くのが正解なのか。
シニアとして後輩に胸を張って見せられる、美しく、かつブラウザに優しい実践的なコードパターンを授けよう。

パターンA:フラットに保つ(`:is()` は最大1階層まで)

機能擬似クラスを使うときは、基本的にネストさせない(平坦に並べる)のが鉄則だ。

/ ❌ やってはいけない:無駄にネストした読みにくいコード /
:is(.card, .panel) :is(.header, .body) :is(h2, h3) {
font-weight: bold;
}

/ ⭕ 正解:条件をフラットに整理し、ブラウザの負担を減らす /
:is(.card, .panel) :is(.header, .body) h2,
:is(.card, .panel) :is(.header, .body) h3 {
font-weight: bold;
}

/ あるいは、さらにシンプルにタグを限定できるならこうする /
:is(.card, .panel) :is(.header, .body) :is(h2, h3) {
/ ※どうしてもこれが必要な場合は1階層に留め、特定性のインフレを防ぐ /
}

おいおい、下側のコードは結局長くなっているじゃないか、と思ったかい?
そうだ。だが、CSSの可読性とパフォーマンスのバランスにおいて、「複雑なネストによる計算コストの増大」を避ける方が、長期的な保守性においては遥かに価値があるんだよ。

パターンB:`:where()` のオーバーライド性をハックする

コンポーネントライブラリや、後からスタイルを上書きさせたいユーティリティクラスを作る場合は、特定性をゼロにする ` :where() ` を使うのが定石だ。

しかし、ここにも罠がある。` :where() ` をネストさせると、特定性が永久に `0` のまま固定されるため、いざというときに意図したスタイルが効かなくてデバッグ地獄に陥る。

/ 悪い例:全部 :where() で囲むと、詳細度のコントロールが効かなくなる /
:where(.modal) :where(.content) :where(p) {
color: #666;
}

これを防ぐためのベストプラクティスは、「大元のセレクタにのみ `:where()` を使い、詳細な条件指定には通常のクラスや `:is()` を組み合わせる」ことだ。

/ 現場で使えるベストプラクティス /
:where(.theme-dark) .card {
background-color: #1a1a1a;
color: #f5f5f5;
}

/ ダークモード内でのみ、特定の要素をハイライトしたい場合 /
:where(.theme-dark) .card :is(h1, h2, h3) {
color: #ff3366; / :is() を使うことで、適度な特定性を担保しつつスマートに記述 /
}

これなら、`.theme-dark` という大元の指定は特定性 `0`(どこからでも簡単に上書き可能)でありながら、カード内の見出しに対するスタイルは確実かつ効率的に適用される。ブラウザの評価木も非常にシンプルな形で処理できるため、レンダリングの負荷を最小限に抑えられる。

—

4. チーフアーキテクトからのまとめ

CSSは「動けば何でもいい」の世界じゃない。
ブラウザという限られたリソースの上で、DOMツリーとスタイルのマッチングを毎秒何回も行わせている、立派な「プログラム」だ。

  • ` :is() ` や ` :where() ` は強力な武器だが、ネストさせるとブラウザのセレクタマッチングに余計な負荷をかける。
  • 基本は「フラットに書く」。ネストさせるとしても原則1階層までにする。
  • コンポーネントの設計思想に合わせて、特定性をコントロールするために使い分ける。

この感覚を持てるようになれば、君が書くCSSはただ動くだけでなく、パフォーマンスに配慮された「プロの仕事」にランクアップする。
さあ、明日からのコードレビューでは、後輩たちの無駄なネストを優しく、しかし厳しく指摘してやってくれよ。期待しているぞ!

コメント

タイトルとURLをコピーしました