TL

CSSセレクタのマッチングと右から左の評価

セレクタが重いと感じる前に、ブラウザがどこで時間を使うかを正しく見積もれる。右端から評価する理由、has のコスト、再計算を一部に閉じ込める無効化の仕組みを原理から解説します。

応用CSSセレクタスタイル計算ブラウザパフォーマンス最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.ブラウザはセレクタを右端のキー(rightmost compound)から左へ評価する。要素ごとに「このセレクタは私に当たるか」を判定するため、まず最右端で大半を弾き、祖先や兄弟をたどるのは候補が残ったときだけにするのが最速だから。
  • 2.:has() は左方向ではなく子孫・兄弟側を見る関係セレクタで、変更の影響が右ではなく上流へ波及する。素朴な実装ではコストが高く、ブラウザは対象スコープを絞る最適化(invalidation set)で実用化している。
  • 3.DOM変更時はツリー全体を再マッチせず、変わった要素と関係し得るルールだけを無効化する。クラス名・属性・状態ごとにどのセレクタが影響を受けるかを事前索引(invalidation set)化し、再計算を局所に閉じ込める。

問いの立て方:マッチングは「要素起点」で行われる

CSSのセレクタを読むとき人間は左から「.menu の中の .itema」と読みます。しかしブラウザのスタイル計算(style recalc)は逆向きに動きます。スタイル計算の仕事は、ルールごとに「どの要素に当たるか」を探すのではなく、要素ごとに「この要素に当たるルールはどれか」を確定することです。DOMの各要素について該当ルールを集め、勝者を カスケード で選び、計算値を決めます。この「要素起点」という視点が、右から左の評価を理解する鍵です。

ある要素 a に対して .menu .item a が当たるかを判定する素直な方法を考えます。左から読むなら、まず文書中の全 .menu を探し、その子孫の .item を探し、さらにその子孫の a を探して、対象要素に行き着くかを確かめることになります。これは要素1個の判定なのに、文書を広く走査してしまいます。要素の数だけこれを繰り返せば計算量は爆発します。

右から左に評価する理由:最右端で大半を捨てる

そこでブラウザは、セレクタを最右端の複合セレクタ(rightmost compound selector、キーセレクタ)から左へ評価します。.menu .item a なら、まず対象要素自身が a かを見ます。違えばその場で不一致が確定し、.menu.item をたどる必要は一切ありません。

.menu .item a   を要素 X について判定する

右端から:
  1. X 自身は a か?        → No なら即不一致(祖先を一切見ない)
  2. X の祖先に .item があるか? → 結合子 ' '(子孫)を上向きにたどる
  3. その上に .menu があるか?   → さらに上向きにたどる
最初に満たせなくなった時点で不一致を確定

要点は、最右端の判定が最も安く、かつ大多数の要素をここで弾けることです。文書中の大半の要素は a ですらないので、ステップ1で O(1) に近いコストで不一致が決まります。祖先方向の走査(コストの高い部分)は、最右端を通過したごく一部の要素についてだけ発生します。左から評価すると不一致の要素にも全工程を払いますが、右から評価すれば**早期棄却(early rejection)**が効きます。

結合子は「上流をたどる」方向を決める

子孫結合子 ' ' と子結合子 > は祖先方向(親をたどる)、隣接 + と一般兄弟 ~ は左の兄弟方向に評価が進みます。いずれも最右端のキーから出発し、結合子が指す向きへDOMを遡って条件を確かめます。子結合子 > は親1段だけを見れば済むため、子孫結合子 ' ' のように祖先を上限まで遡る可能性がない分、最悪計算量が小さくなります。

ブラウザはさらに、最右端のキーをセレクタの種類別(タグ名・クラス・ID)にバケット化した索引を持ちます。要素にルールを照合するとき、その要素のタグ名・各クラス・IDに一致し得るルールだけを索引から引き、無関係なルールは最初から候補にしません。「右から評価」と「キーによる事前バケット化」が組み合わさることで、要素あたりの照合がルール総数に比例しなくなります。

セレクタの「速さ」の実像

この仕組みから、セレクタのコストは左側の長さよりも、最右端のキーがどれだけ選択的(selective)かで大きく決まることが分かります。#id や限定的なクラスがキーなら候補は少なく、汎用的なタグや * がキーだと候補が増え、祖先走査の回数も増えます。

セレクタ最右端キーコスト傾向
#sidebar aa(汎用的)a が多いと候補が多く祖先走査も増える
a.externala.external(クラス付き)external 保持要素のみが候補で絞り込みが効く
nav > ul > li > aa結合子は全て > で親1段ずつ、最悪走査は浅い
div .x .y .z spanspanspan が多いと候補多・祖先深さも効く

ただし現実のページでは、こうしたセレクタ単体のコスト差が体感差になることは多くありません。スタイル計算の総コストを支配するのは多くの場合何回・何要素ぶん再計算が走るかであり、個々のセレクタの微妙な速さではありません。最適化の主戦場はセレクタの書き換えより、後述する無効化の局所化です。

:has() のコスト:影響が「上流」へ波及する

:has() は関係セレクタ(relational pseudo-class)で、a:has(> img) のように子孫・兄弟側の条件で左の要素を選ぶものです。ここで評価の向きが反転します。通常のセレクタは右端の要素が変わったとき影響を右端に閉じ込められますが、:has() では子孫や後続兄弟の変化が、上流の(左の)要素のマッチ状態を変え得るのです。

li:has(.active) { ... }

.active が li の子孫で付け外しされると、
影響を受けるのは .active 自身ではなく「祖先の li」。
つまり変更点から上流・側方へ無効化が広がる。

素朴に実装すると、.active の付け外しのたびに「この変化で :has() のマッチが変わる祖先はどこまでか」を調べ直す必要があり、最悪では祖先方向や兄弟方向に走査が広がってコストが高くなります。ブラウザはこれを実用化するために、:has() を含むルールを変化の種類ごとに索引化し、:has() の引数に現れる特徴(クラス・タグ・状態)が変わったときだけ、対応する祖先側のスコープに無効化を限定します。

:has() は「安いから自由に使え」ではない

現在のブラウザは :has() 向けの無効化最適化を備えており、多くのケースで実用的な速度が出ます。ただしコストがゼロになったわけではありません:has() の引数が広く頻繁に変化する特徴(例:ツリー全体で付け外しされる状態クラス)を参照していると、無効化の波及が広がり再計算量が増えます。引数はできるだけ選択的にし、頻繁に変わる状態を広いスコープの :has() で監視しない設計が安全です。

無効化(invalidation)の局所化:ツリー全体を再マッチしない

DOMやスタイルが変わるたびに全要素を全ルールで再マッチしていたら、わずかな class 変更でもページ全体のスタイル計算が走ってしまいます。これを避ける仕組みがスタイル無効化(style invalidation)です。中心となるデータ構造が無効化集合(invalidation set)で、「ある特徴(クラス名・属性・擬似クラス状態)が変化したとき、影響を受け得るルール/要素はどれか」を事前に索引化しておきます。

ルール解析時に索引を構築:
  .open    → { .open を含むルール群 }
  [disabled] → { 属性 disabled を見るルール群 }
  :hover   → { hover に依存するルール群 }

要素 X の class に open が付いた:
  1. invalidation set から .open に紐づくルールを引く
  2. 影響範囲(X 自身か、子孫か、兄弟か)を結合子から決める
  3. その範囲の要素だけスタイルを再計算

ポイントは、変化した特徴をキーに影響範囲だけを引き当てることです。classopen が1つ付いただけなら、.open を参照するルールに関係する要素しか再計算されません。文書の他の部分は一切触りません。これが再計算の局所化で、インタラクションのたびに全ツリーを舐めないための核心です。

変化の種類無効化の起点波及方向
要素自身のクラス追加その要素自身+(子孫/兄弟結合子があれば)下流・側方
属性値の変更その要素属性セレクタを持つルールの範囲
:hover などの状態状態が付いた要素状態セレクタの結合子が指す範囲
:has() の引数側の変化引数にマッチした子孫/兄弟上流(祖先の :has() 要素)

無効化には**子孫無効化(descendant invalidation)**もあります。.list .item のようなルールでは、.list が付いた要素の子孫の .item が影響を受けるため、起点要素の子孫方向へ無効化を広げます。ブラウザは結合子から「自分だけで済むか/子孫まで波及するか/兄弟まで波及するか」を判定し、必要最小限のスコープにとどめます。

無効化を狭く保つ設計

頻繁に切り替わる状態クラスは、できるだけ末端の要素に付けるほど無効化が浅く済みます。ルート近くの要素のクラスを切り替えると、それに連なる子孫無効化が広く走り得ます。「状態は深く、構造は浅く」を意識すると、同じ見た目でも再計算スコープを小さく保てます。これはセレクタを短く書くこと以上に効く実務的な勘所です。

スタイル計算はパイプラインのどこにあるか

ここまでの照合と無効化は、ブラウザのレンダリングにおけるスタイル(recalc styles)段で起こります。各要素の計算値が確定すると、その結果は次段のレイアウトへ渡され、サイズや位置が解決されます。スタイル計算で再計算スコープを小さく保てても、確定した値がレイアウトに影響するプロパティを含めば、下流でリフローが走ります。

押さえどころ

頻出は次の点です。(1) マッチングは要素起点で、セレクタは最右端の複合セレクタから左へ評価し、最右端で早期棄却するから速い。(2) コストは左の長さより最右端キーの選択性と結合子の向き(> は親1段、' ' は祖先まで)で決まる。(3) :has() は影響が上流(祖先)へ波及する関係セレクタで、ゼロコストではなく無効化最適化で実用化されている。(4) DOM/状態変化時は全再マッチではなく、invalidation set で変化した特徴に紐づくルール・範囲だけを無効化する。「セレクタは左から照合する」「子孫セレクタは祖先を全部探す」は典型的な誤りです。

まとめ

まとめ

ブラウザのスタイル計算は要素起点で動くため、セレクタを最右端の複合セレクタ(キー)から左へ評価します。大多数の要素は最右端の判定で不一致が確定し、祖先・兄弟方向の走査は通過した一部にしか発生しないため、これが最速になります。コストは左側の長さより最右端キーの選択性と結合子の向きで決まります。:has() は影響が上流へ波及する関係セレクタで、ブラウザは変化の種類ごとの索引で無効化スコープを絞って実用化していますが、引数が広く頻繁に変わると再計算が増えます。DOMや状態が変わったときは全要素を再マッチせず、invalidation set(クラス・属性・状態をキーにした事前索引)で影響範囲だけを再計算し、局所化します。実務では個々のセレクタを縮めるより無効化スコープを浅く保つ設計が効きます。前提となる見た目の指定は CSS、照合後の勝敗決定は CSSカスケード・詳細度、これら全体が走る場所は レンダリングパイプライン詳説、下流のリフロー/リペイントの分類は リフローとリペイントのコスト で押さえると、セレクタの一文字から画面更新までが一本につながります。

Web/フロントエンド Article

CSSセレクタのマッチングと右から左の評価を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

CSS

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

:has() は左方向ではなく子孫・兄弟側を見る関係セレクタで、変更の影響が右ではなく上流へ波及する。素朴な実装ではコストが高く、ブラウザは対象スコープを絞る最適化(invalidation set)で実用化している。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「CSS / セレクタ」に近いか確認する。
  • 強みである「ブラウザはセレクタを右端のキー(rightmost compound)から左へ評価する。要素ごとに「このセレクタは私に当たるか」を判定するため、まず最右端で大半を弾き、祖先や兄弟をたどるのは候補が残ったときだけにするのが最速だから。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

CSSセレクタスタイル計算ブラウザパフォーマンスCSSセレクタスタイル計算
参考: 公式情報