jQuery を高速に使う CSS セレクタの書き方

jQuery は CSS セレクタで要素を選んで処理できるのが魅力的ですね。そんな jQuery ですが、CSS セレクタの書き方次第で速度が大幅に変わってきます。

ここでは jQuery の内部処理を疑似コードで示しつつ、jQuery を高速に使うためのポイントを5つに絞って紹介します。

  1. 何度も同じセレクタを実行しない
  2. クラスだけを指定するのは禁止
  3. #id を積極的に使う
  4. 途中までの結果を再利用する
  5. 子供セレクタ(>)を使うと速くなることがある

※ この記事は jQuery 1.2.6 のソースコードを元に記述しています

1. 何度も同じセレクタを実行しない

改善前

// 例題 1
$("div.foo").addClass("bar");
$("div.foo").css("background", "#ffffff");
$("div.foo").click(function(){alert('foo');});

何が問題か

jQuery は CSS セレクタを書くたびに、DOM をたどってセレクタにマッチする要素を検索します。

$("div.foo") を実行すると、その背後で jQuery は次のような処理を実行しています。

// セレクタで選択した結果を格納する配列
var ret = [];

// div タグ一覧を列挙する
var elems = document.getElementsByTagName("div");

// それぞれについて、クラス名が foo のものを ret に入れる
for(var i = 0; i < elems.length; i++){
    var classes = elems[i].className.split(" ");
    if(classes.indexOf("foo") != -1){
        ret.push(elems[i]);
    }
}

HTML 中に含まれる div タグを列挙して、そのそれぞれについてクラス名を調べていくわけです。(Array.indexOf は非標準ですが、簡単に書くために使っています)

つまり、冒頭のコードのように $("div.foo") を3回書いてしまうと、上記の処理が3回実行されてしまいます。非効率的ですね。

改善方法1: キャッシュ

セレクタの実行結果を変数にキャッシュしておきます。2回分の $("div.foo") 実行時間が節約できます。

// コード 1-1
var foos = $("div.foo");
foos.addClass("bar");
foos.css("background", "#ffffff");
foos.click(function(){alert('foo');});

改善方法2: メソッドチェーン

メソッドチェーンを使うと、jQuery っぽくなりますし、処理効率も上がります。

// コード 1-2
$("div.foo")
    .addClass("bar")
    .css("background", "#ffffff")
    .click(function(){alert('foo');});

$("div.foo")セレクタの実行結果が次のメソッドに順番に引き継がれます。一時変数を必要としないのも嬉しいところです。

2. クラスだけを指定するのは禁止

改善前

// 例題 2
$(".foo").css("display", "none");

何が問題か

クラス名だけを指定すると、jQuery は全ての HTML ノードを列挙して、そのそれぞれについてクラス名を調べます。

$(".foo") の背後は次のような処理が実行されます。

// セレクタで選択した結果を格納する配列
var ret = [];

// 全てのタグを列挙する
var elems = document.getElementsByTagName("*");

// それぞれについて、クラス名が foo のものを ret に入れる
for(var i = 0; i < elems.length; i++){
    var classes = elems[i].className.split(" ");
    if(classes.indexOf("foo") != -1){
        ret.push(elems[i]);
    }
}

全てのタグを列挙してループを回すわけですから非効率的ですね。

少し、話がそれますが、Firefox3 や Opera9.5、Safari3 には getElementsByClassName() メソッドがネイティブ実装されています。そのため、これらのブラウザは高速に $(".foo") を実行できる能力を持っています。しかし、jQuery 1.2.6 の時点では、ネイティブの getElementsByClassName() を使っていません。

jQuery の次期 CSS セレクタである Sizzle では、getElementsByClassName() が定義されていれば利用するよう実装されているようです。

改善方法: タグを併記する

タグを明示します。

$("div.foo").css("display", "none");

全てのノードからではなく指定したタグの中からクラス名で絞り込むようになるため、ループの回数が大幅に削減されます。

3. #id を積極的に使う

改善前

<!--例題 3-->
<body>
<script src="jquery.js"></script>
<script>
$(function(){
    $(".main").css("color", "red"); // ← ココ
});
</script>
<div class="main">
  < ... >
</div>
</body>

何が問題か

先ほども述べましたが、jQuery ではクラス名での探索は非効率的です。

HTML の設計の話になってしまいますが、HTML 中で1度しか登場しないクラス名は id にしてしまってもよいでしょう。そのほうが JavaScript で扱うにも好都合です。

改善方法

main をクラスではなく id に変更します。

<!--例題 3-->
<body>
<script src="jquery.js"></script>
<script>
$(function(){
    $("#main").css("color", "red"); // ← ココ
});
</script>
<div id="main">
  < ... >
</div>
</body>

jQuery は、セレクタに id が指定されていた場合には、再帰的に探索せずに getElementById() を利用します。そのため、全ノードを列挙するのに比べ、格段に高速に処理できます。

4. 途中までの結果を再利用する

改善前

<body>
<script src="jquery.js"></script>
<script>
$(function(){
    $("#main div.entry").css( ... );
    $("#main div.entry div.body")  // ← ココ
        .css( ... );
});
</script>
<div id="main">
  <div class="entry">
    <div class="header"> ... </div>
    <div class="body"> ... </div>
  </div>
  <div class="entry">
    <div class="header"> ... </div>
    <div class="body"> ... </div>
  </div>
</div>
</body>

どこが問題か

ここまで読んでこれば既にお分かりかもしれません。

    $("#main div.entry")              // (A)
    $("#main div.entry div.body")     // (B)

(B) のセレクタでは、

  1. #main を探す
  2. その子孫から div.entry を列挙する
  3. その子孫から div.body を列挙する

という処理を行います。このうちの、1. と 2. は (A) と全く同じ処理です。(A) で求めた結果を再利用すれば処理速度は向上するはずです。

改善方法

キャッシュ作戦です。

// (A) で列挙されたタグを変数に格納
var entries = $("#main div.entry").css( ... );

// $() の第2引数に (A) の結果を渡す
$("div.body", entries).css( ... );

$() 関数の第2引数には探す基点を指定することができます。(A) の結果に含まれる要素の子孫から div.body を探してくれます。

find() メソッドを使ってもよいでしょう。

var entries = $("#main div.entry").css( ... );
entries.find("div.body").css( ... );

おっと、ここまでくればメソッドチェーンができそうですね。

$("#main div.entry").css( ... )
    .find("div.body").css( ... );

応用例

div.head も探したい場合にはどうすればよいでしょう。

はい、こうすればよいですね。

var entries = $("#main div.entry").css( ... );
entries.find("div.body").css( ... );
entries.find("div.head").css( ... );

こいつもメソッドチェーンしてしまいましょう。end() を使えば、find() で探す前の状態に戻すことができます。

$("#main div.entry")
    .css( ... );
    .find("div.body") // #main div.entry div.body になる
        .css( ... )
    .end()            // #main div.entry に戻る
    .find("div.head") // #main div.entry div.head になる
        .css( ... )
    .end();

ここまで来るとアクロバティックですが…一番最初のコードより高速なのは間違いありません。

5. 子供セレクタ(>)を使うと速くなることがある

改善前

<!--例題 5-->
<body>
<script src="jquery.js"></script>
<script>
$(function(){
    $("#main div.entry").css( ... ); // ← ココ
});
</script>
<div id="main">
  <div class="entry">
    <div class="header"> ... </div>
    <div class="body"> ... </div>
  </div>
  <div class="entry">
    <div class="header"> ... </div>
    <div class="body"> ... </div>
  </div>
</div>
</body>

どこが問題なのか

「#main div.entry」は #main のあとに子孫セレクタ(スペース)があります。つまり、#main ノードの下の全ての div ノードから entry クラスを探し出します。

$("#main div.entry") の背後では次のような処理が実行されています。

// セレクタで選択した結果を格納する配列
var ret = [];

// #main を探す
var main = document.getElementsById("main");

// #main の配下から全ての div を列挙する
var elems = main.getElementsByTagName("div");

// それぞれについて、クラス名が foo のものを ret に入れる
for(var i = 0; i < elems.length; i++){
    var classes = elems[i].className.split(" ");
    if(classes.indexOf("foo") != -1){
        ret.push(elems[i]);
    }
}

elems には div#main の下の全ての div タグが格納されます。この全てについてクラス名を確認するわけですら、場合によっては遅くなってしまいます。

もし、div.entry が div#main 直下にのみ存在するのであれば、「子孫セレクタ」ではなく「子供セレクタ」を使えば効率的に動作するかもしれません。

改善方法

子供セレクタ(>)を使います。

    $("#main > div.entry").css( ... );

jQuery では子供セレクタが出てくると、全ての子孫ではなく、子供の中からマッチするものを調べます。孫やその子供については調査しないため、高速化が期待されます。

$("#main > div.entry") の背後では次のような処理が実行されます。

// セレクタで選択した結果を格納する配列
var ret = [];

// #main を探す
var main = document.getElementsById("main");

// #main の子ノードの中から
// タグ名が DIV であり、クラス名が entry のものを
// ret に入れる
var child = main.firstChild;
while(child){
    var classes = elems[i].className.split(" ");
    if (child.tagName == "DIV"
    && classes.indexOf("entry") != -1){
        ret.push(child);
    }

    child = child.nextSibling;
}

子供セレクタを使えば必ず速くなるというわけにはいきませんが、子供の数に比べて子孫が大量にいる場合には、子供セレクタのほうが速くなります。

実際のコードで試す

実際にブラウザで実行したときに CSS セレクタによって処理速度がどれだけ改善するかを確認してみましょう。

試験はこのブログの HTML で実行してみました。HTML 構造はこんな感じです。

<div id="days">
    <div class="day">
        <h2>2008年12月11日</h2>
        <div class="body">
            <div class="section">
                <h3>タイトル</h3>
                <p>本文</p>
            </div>
        </div>
    </div>
    <div class="day"> ... </div>
    <div class="day"> ... </div>
</div>

このような HTML に対して、jQuery を実行してみました。

CSS セレクタFirefox2IE7Opera9Safari3(Win)
.body22.18ms19.85ms5.32ms2.49ms
div.body2.34ms2.82ms1.24ms0.49ms
#days > div.day > div.body2.66ms1.72ms1.25ms0.44ms
  • 全てのブラウザで、.body に比べて div.body の方が5~10倍速くなっている。
  • #days > div.day > div.body は子供セレクタを2回使っているのに、div.body と同じぐらいの速度で実行できている。

最後に

jQuery はライブラリである以上、DOM を直接さわるのに比べて遅くなることは避けられません。

どうしても処理速度が気になる場合は、jQuery のコードを DOM を直接さわるコードに変換するとよいでしょう。経験的に Firefox や IE で処理速度が10倍ぐらいになります。

ただし、開発効率の面からも、最初は jQuery を使って書き始めることをお薦めします。jQuery のコードを DOM 直接に変換するのは簡単ですが、DOM 直接で開発を進めるのはめんどくさいですよね。