話題の記事

HTML5 × CSS3 × jQueryを真面目に勉強 – #7 jQueryのセレクタAPIについて詳しく

2012.10.19

そんな訳で、普段何気なく使っているjQueryですが、そのセレクタAPIについて多少は知っておいたほうが良いよなということで、いくらか調べてみました。けっこう地味な内容なので、「へー、そんな風になってるんだぁ…」と軽く読み流していただければと思います嘘です。割と大事な内容なので、しっかりと把握しておくのがよろしいかと思います。

はじめに - jQueryのセレクタAPI

jQueryでは、$('#hoge .fuga');というようにCSSのセレクタを用いてHTML要素を取得します。あまりにも便利な機能で普段意識することはありませんが、内部ではgetElementById();といったブラウザのネイティブAPIを駆使したり、JavaScriptゴリゴリのメソッドを呼びまくって指定どおりの要素を取得してきているわけです。こういった機能のことをセレクタAPIと呼びます。

セレクタAPIの内訳

jQueryのセレクタAPIは、主に以下のような感じで分類することが出来ます。

  1. ブラウザのネイティブ機能
    • getElements 系
    • querySelectorAll
  2. Sizzle

 ブラウザのネイティブ機能

皆さんがJavaScriptを習得する際に、早い段階で覚えるであろうgetElementById()getElementsClassName()といったAPIの事を指します。

  • getElementById(selector)
  • getElementsByClassName(selector) ※1
  • getElementsByName(selector)
  • getElementsByTagName(selector)
  • querySelectorAll(selectors) ※次項にて解説

※1 ちなみにIE8はgetElementsByClassName();をサポートしていません。IE9よりサポートされています。
W3C DOM Compatibility - Core

querySelectorAll()

querySelectorAll()とは、jQueryと同じような書式のCSSセレクタで要素を指定することが可能なブラウザのネイティブAPIで、比較的新しくサポートされました。

querySelectorAll()は渡されたCSSセレクタにマッチした要素全てを配列で返し、類似のAPIであるquerySelector()は最初にマッチした要素だけを返します。

// #hoge .fugaに最初に該当する要素のみを返します
document.querySelector('#hoge .fuga');

// #hoge .fugaに最初に該当する要素すべてを配列形式で返します
document.querySelectorAll('#hoge .fuga');

サポートするブラウザは以下のとおり。

サポートするブラウザは以下のとおり
Internet Explorer Firefox Chrome Safari Opera Android iOS
8 ~ 3.5 ~ 1 ~ 3.2 ~ 10 ~ 2.1 ~ 3.2 ~

Sizzle

Sizzleとは、CSSセレクタを受け取ってそれに該当する要素を返すセレクターエンジンのことで、jQuery開発者であるJohn Resig氏によって作られたJavaScriptライブラリです。jQuery 1.3より実装されました。その後jQueryのバージョンアップとともに何度かのパフォーマンスチューニングが施されており、jQuery 1.8においても大規模なテコ入れがなされました。

querySelectorAll()がサポートされていないブラウザ用にCSSセレクタを実現するためのエンジンというのが当初の位置づけでしたが、querySelectorAll()だけではカバーしきれないjQuery独自のセレクタにも対応するための機能ということで、現在まで継続して実装されています。

jQueryの独自セレクタ(フィルタ) ※一部を抜粋
jQuery セレクタ構文 選択対象
:first 最初に選択された要素
:last 最後に選択された要素
:even 偶数要素(インデックスは0スタート)
:odd 奇数要素(インデックスは0スタート)
:eq(n) インデックス(n)を元に1つの要素る
:lt(n) インデックスがnよりも小さい全ての要素
:gt(n) インデックスがnよりも大きい全ての要素
:animated アニメーションが開始されている要素
:visible 可視状態となっている要素
:input input、textarea、button、select要素
:password <input type="password" />
:checkbox <input type="checkbox" />
:radio <input type="radio" />
:hidden <input type="hidden" />

これらがひとつでもセレクタに含まれていると、Sizzleが呼び出されます。

jQueryが実際に呼び出すセレクタAPI

jQueryに渡されたセレクタに応じて内部でAPIが呼び出されるわけですが、どのAPIが呼び出されるかは、以下の優先度に従って決定します。

  1. 単一のID名やCLASS名の場合は、getElementById()やgetElementsByClassName()といった高速APIを使用
  2. (1)では処理しきれないが、querySelectorAll()が正常に通る場合は、これを使用
  3. (2)でも処理しきれない場合はSizzleを使用

ちなみにIE~7といったquerySelectorAll()をサポートしていないブラウザの場合は、(1)が通らなかった時点でSizzleが使用されます。

各セレクタAPIのベンチマークをとってみた

理屈だけ押し並べてもアレなので、即席のサンプルを作ってどれくらい速さに差がでるのか計測してみました。

実行環境
OS | Windows 7 64bit
CPU | Intel Core2 Quad Q6600
RAM | 4.00GB
ブラウザ | Chrome 22.0.x
計測内容
jQueryとブラウザネイティブによるセレクタAPI呼び出しを1万回ずつ実行するのに要する時間を計測

 実行結果がコチラ

では順番に見ていきます。

$('div#hoge');  // 131ms

$('div#hoge');と指定することによって、jQuery内部ではquerySelectorAll();が呼び出されました。遅いです。

$('#hoge'); // 24ms

$('#hoge');と指定することによって、jQuery内部ではgetElementById();が呼び出されました。速いです。

$('div.fuga');  // 119ms

$('div.fuga');と指定することによって、jQuery内部ではquerySelectorAll();が呼び出されました。遅いです。

$('.fuga'); //87ms

$('.fuga');と指定することによって、jQuery内部ではgetElementsByClassName();が呼び出されました。けっこう速いです。

$('#hoge:first'); // 606ms

$('#hoge:first');と指定することによって、jQuery内部ではSizzleが呼び出されました。がんばってるのは分かりますが、やっぱり遅いです。

続いてはjQueryを介さずに直接JavaScriptにてブラウザのネイティブAPIを呼び出した場合の結果ですが、

getElementById('hoge');     // 1ms
getElementsByClassName('fuga'); // 3ms
querySelectorAll('#hoge');    // 14ms
querySelectorAll('.fuga');    // 21ms

すごく・・・、速いです・・・。

僕自身、実際に計測するまでは何となく速いんだろうなぁ、という程度にしか思っていませんでしたが、ここまで差が出るとは予想外でした。

補足 - コンテキストパラメータを使用した際のベンチマーク

$()には、第二引数にコンテキストを渡すことで、セレクタに指定された要素を検索する範囲をします。デフォルトはdocumentとなっており、DOM階層全体から検索されます。
http://s3pw.com/qrefy/jquery/

そんな訳で、コンテキストパラメータを指定した場合のベンチマークをとってみて、それに対する最適化の方法をメモっておくとします。

実行結果がコチラ

// コンテキストパラメータを使用
$('p', '#hoge');  // 155ms

コンテキストを指定するとjQUery内部でパラメータを解析し、コードを最適な式に変換した後に処理されます。なので遅いです。

// コードを最適化
$('#hoge').find('p'); // 116ms

jQuery内部でコードが自動変換されると書きましたが、その変換結果が正にこのコードであるため、最初から変換後のコードを記述することで、僅かながらパフォーマンスが向上しているのが分かります。

以上のことから、コンテキストパラメータを使用するくらいならば、findメソッドを活用するほうがSizzleの呼び出しや余計な処理をせずに済む品質の高いコードが書けるということが言えます。

おわりに

以上のことからjQueryから呼ばれたセレクタAPIの結果は、たとえ内部でネイティブAPIが実行されていたとしても、純粋にgetElementById()を実行した結果と比較してパフォーマンスに差ががあるのが証明されました。Sizzleに至ってはどうしても複雑な処理を行う以上、超えられない壁というのが出てきてしまうのは仕方ないね。

ただ誤解してはいけないのが、jQueryイラネというわけでは決してありません。むしろあってくれないと僕が一番困ります。Sizzleなしに複雑な条件の要素を取得するとなると、可読性の悪いコードが出来上がるだけで、何一つ良いことはありません。

今回の考察はjQueryをより深く理解する為に必要なステップであり、これを踏まえることで可読性およびパフォーマンス共に、より効率性の高いコードが書けることは間違いないはずです。

参考サイト