Sizzleセレクタエンジンは内部で何をしているのか

Sizzleは「要素検索」ではなく「クエリエンジン」

jQueryのセレクタは単にdocument.querySelectorを呼んでいるだけ、と思われがちです。
しかしjQueryの心臓部であるSizzleは、単なる検索関数ではありません。

SizzleはDOMに対するクエリエンジンです。

つまり「要素を探す関数」ではなく、
「セレクタという問い合わせ文を解釈し、最適な探索計画を立てて実行する仕組み」です。

これはデータベースのSQLエンジンに近い考え方です。
セレクタ文字列は、DOMに対するクエリ言語として扱われています。

セレクタはそのまま使われていない

次のコードを見ます。

$("div#menu li.active a");

Sizzleはこの文字列をそのまま検索しません。
内部ではまず解析(パース)を行います。

処理の流れは大きく4段階あります。

1. トークン化
2. コンパイル
3. 探索計画の決定
4. 実行とフィルタリング

ここがjQueryのパフォーマンスの鍵です。

1. トークン化(Tokenizer)

最初にセレクタを分解します。

例:

div#menu li.active a

は次のような部品に分かれます。

トークン 意味
div タグ
#menu id
li 子孫タグ
.active class
a タグ

Sizzleは正規表現で解析し、
構文木に近いデータ構造へ変換します。

ここで初めて「何を探しているか」を理解します。

2. コンパイル

次にSizzleはセレクタを関数へ変換します。
これをコンパイルと呼びます。

つまりSizzleは毎回文字列を解釈しているわけではありません。
一度解析したセレクタはキャッシュされ、
実行可能な検索関数になります。

概念的には次のような形です。

function matcher(elem){
  return elem.tagName === "A" && elem.classList.contains("active");
}

これにより同じセレクタを何度使っても高速に処理できます。

3. 探索計画の決定

ここが重要な部分です。
SizzleはDOMを先頭から走査しません。

最も絞り込める条件から探索を始めます。

例:

$("div#menu li.active a");

この場合、最初に#menuを取得します。

理由は単純で、idは一意だからです。
DOM全体を探すより圧倒的に効率的です。

つまりSizzleは
最小コストの探索ルートを選択するように設計されています。

4. 実行とフィルタリング

探索結果はそのまま返されません。
最後にフィルタリングが行われます。

ここで次が評価されます。

  • 親子関係
  • 擬似クラス
  • 属性条件
  • 表示状態

例:

$("li:first");

これはDOM構造ではなく、
取得結果の配列に対するフィルタです。

つまりSizzleは

検索 → 絞り込み → 条件評価

というパイプライン処理をしています。

なぜここまで複雑だったのか

理由はブラウザのばらつきです。
当時の問題は次の通りでした。

  • querySelectorがない
  • XPathが不統一
  • DOM実装差
  • XMLとHTMLの違い

そのためjQueryは
「どのブラウザでも同じセレクタが同じ結果を返す」
ことを最優先にしました。

Sizzleはその互換性を実現するための仕組みです。

現在のブラウザとの関係

現在のブラウザにはquerySelectorAllがあります。

document.querySelectorAll("div.active");

ネイティブ実装はブラウザ内部(C++)で実行されます。
そのため基本的にはこちらが高速です。

ただしjQueryは現在もSizzleを完全に捨てていません。
理由はjQuery独自セレクタです。

  • :visible
  • :contains
  • :animated

これらはCSS仕様外のため、
最終的なフィルタリングはSizzleが担当します。

注意点

複雑なセレクタを多用するとパフォーマンスが低下します。

$("body div ul li a span.active");

これは1回の検索ではなく、多段探索です。
ループ内で使うと顕著に遅くなります。

解決策はキャッシュです。

var menu = $("#menu");
menu.find("a.active");

探索範囲を限定するだけで負荷が減ります。

まとめ

Sizzleは単なるセレクタ処理ではありません。

  • セレクタを解析する
  • 検索関数へコンパイルする
  • 最適な探索順序を選ぶ
  • 結果をフィルタする

つまりDOMに対するクエリエンジンとして動作しています。

jQueryが当時安定して動いた理由は、
便利な記法ではなくブラウザ差を吸収する内部エンジンが存在したからです。

そして現在のquerySelectorAllが当たり前に使えるのは、
こうした仕組みをライブラリが先に実現していた歴史の延長と言えるでしょう。