Selectors API以降のセレクタ実装 - jQueryとSizzleとuupaa-selectorとkQuery

低速復活中なので、kQueryを細々と書き直しました。

URLからもわかるように大好きなSlickSpeedも置いておきました。

広告入るスペースなので、zipに固めたものも置いておきます。

基本的に速度向上ではなくて、内部的なリファクタリングがメインなので、劇的に速度が向上したりはしていません。まぁ、少しは向上していますがw

今回の改造は、

  • 親ノードを配列で複数渡せるように。
    • 大抵のセレクタ実装だと一つの親しか渡せないけど、find関数を実装することを考えると複数取れた方が都合がいいよね?
  • ID、クラス、属性セレクタ、疑似クラス部分を関数で切り出し。
    • filter関数を実装する都合です。

という感じです。

さて、上のベンチと名乗るベンチでないSlickSpeedですが、Selectors APIが今後メインになっていくことを考えると、そこまで深く考えなくてもいいような気もします。あと、どのセレクタを多用するかによっても、有利不利が変わる気がします。

そもそも、Selectors API以後のセレクタ実装の役割は

  • Selectors API非対応のブラウザで最後まで残りそうなIE6、IE7向けにSelectors APIと同等の機能を提供する
  • Selectors APIで提供されない、子供セレクタなどから始まるものや独自の疑似クラスへの対応を提供する

という二つの目的に集約されていきそうな気がします。前者を取るか、後者を取るかでも、そのライブラリの方針の違いが感じられそうな気がします。

まぁ、そんなわけで、全体的な傾向として、結果を見ていき、ライブラリごとにどのようなセレクタを書けばいいのかとか、思ったことを書いていこうと思います。

実験環境はIE6.0、Fx3.0(Selectors API無し)。kQueryだけはIE5.0でも動作させておきました。

jQuery 1.2

世界で一番使われているライブラリであるので、一応、これを基準に書いていきたいと思います。

とりあえず、Prototype.jsとかYUI等のメジャーなライブラリの中ではかなり速いです。でも、セレクタの精度は、カンマ区切りで重複を除去しなかったり、疑似クラスへの対応がダメだったりします。

とはいえ、そこに気をつければ、普通に良いセレクタ実装です。#IDやTAGの場合は特に高速に動作するように作られています。そして、何より普通に速いです。kQueryとかuupaaと比較すると遅いだけで、普通に速いです。

あと、実装に掛かっている行数も今回取り上げるものの中では一番短い感じです*1

jQuery
500行
Sizzle
800行
uupaa-selector
1100行
kQuery
750行

まぁ、kQueryはまだjQueryの独自疑似クラスを実装していなかったりするので、単純な規模の比較もできないんですけどねw

まぁ、古いバージョンになりゆくものですから、このくらいにしておきます。一応、これが基準です。

Sizzle (jQuery 1.3)

jQueryの次期セレクタエンジン。でも、キャッシュオフだと、別に速くないというか、細かい点で前のエンジンの方がいいです! というわけで、jQueryの旧エンジンと比べて何が速くなって、何が遅いのか見ていきます。

  • TAGや.CLASSと言ったセレクタIEでも速くなっています。特に.CLASSはgetElementsByClassName対応だとめっさ速くなっています。でも、それだけです。タグ名をつけないとIEだと遅くなり、タグ名つけるとgetElementsByClassNameを使わないので、そんなに速くなりません。あちらを立たすとこちらが立たずの典型例です。そもそも、Selectors APIに対応していなくて、getElementsByClassNameに対応しているブラウザとか、確実にごく少数でしょう。結局、いまいち使える感じがない処理です。
  • #IDの他にTAG#IDは別処理になっているようで、高速に処理できるようです。
  • カンマ区切りされたセレクタが選択した要素の重複チェックは旧来同様行いません。まぁ、行わないことを踏襲したと考えれば、別に気にすることでもないでしょう。
  • 子孫セレクタが遅い。uupaaに負けるとかいう話ではなく、旧エンジンにも負けています。数倍から100倍遅くなります。特に#ID TAGは旧エンジンが速かったこともあり、相当な差がついています。しかも、このパターンはよく使うセレクタだと思います。正式リリースには改善して貰いたい部分です
  • 子供セレクタの早さは異常。やたらと速い。キャッシュ有効なuupaaよりも速い。正直キモイ。
  • 属性セレクタも旧エンジンと同等もしくは若干に速くなっている。ただし、空白が入るとエラー吐くようになってしまった。なんで、微妙に改悪するんだろう。
  • 疑似クラスは以前同様の対応具合の上に、遅くなっている。なんというのか、対応させる気が感じられません。

正直、300行余分に何を書いたのかすごく疑問なバージョンアップだと思いました。

uupaa.js

世界最高の精度のセレクタ。世界で唯一の高度なキャッシュ管理機構。でも、元のエンジンは世界最高速と言えるほど高速じゃない。速く見えるのは、速度比較のわかりやすい手段である、オリジナルのSlickSpeedのセレクタに対して、チューニングを行っているからだったりします。というわけで、というわけで、キャッシュ無しの場合は何をやっているのか、追ってみました。

さて、jQueryやkQueryにもありますが、本格的にセレクタをパースする前に簡単にセレクタをパースする、高速化のための前処理があります。例えば、jQueryだと/^#(\w+)$/にマッチするセレクタはすぐにgetElementByIdで探索するようになっていて、IDの要素はすぐに選択できるようになっています。ちなみにkQueryは#ID、TAG、TAG#IDの場合は高速処理するようにしています。
では、uupaa.jsの場合はと言うと.CLASS、#ID、TAGA,TAGB,TAGC、TAGA,TAGB、TAGA、TAGA TAGB TAGC、TAGA TAGB、:rootと8通りの場合で高速に処理を行えるようにしています。さて、SlickSpeedの場合、どの実装も特に最適化を施していない+、>、~のセレクタと属性セレクタと疑似クラスを使っていないセレクタは13ありますが、uupaa.jsはそのうち8つに対して高速処理するようになっています。

とはいえ、よく出てきそうなセレクタなので、こういうやり方も、まぁ、仕方ないのです。従って、上述した高速処理できないセレクタを書く必要がある場合、どう書けば、uupaa.jsで高速にセレクタを処理するできるかまとめてみました。

  • 普通やりませんが、.CLASSではなく、*.CLASSと書くと二倍遅くなります。というか、多くのセレクターエンジンでは後者の方が遅くなります。例外的にPrototype.jsのみ速くなります。
    • ちなみに#IDではなく*#IDやTAG#IDと書くと遅くなります。例外のエンジンもありますが。
  • TAG.CLASSや.CLASS1.CLASS2のような指定をするとjQueryよりも遅いです。とはいえ、TAG.CLASSは.CLASSよりは速いので、これを使いましょう。
  • 上記の高速化のためのパターンに当てはまらない形での、セレクタのカンマ区切りはタグのマージのため、非常に遅くなります。
  • 上記の高速化の(ry)子孫セレクターは、極端に遅いSizzleほどではありませんが、jQueryより遅いです。Sizzle同様ですが、#ID TAGというパターンが遅いです。
  • 属性セレクタも決して速いとは言いきれません。ですが、TAG[ATTR]がIEでも正確に動作するのはuupaaだけなので、精度は非常に高いです。
  • :linkは単体で使ってはいけないほど遅いです。visitedかどうかまできちんと判別するため、非常に遅くなります。
  • 疑似クラスがさすがに高い精度とかなりの速度を出します。特に:nth-*は非常に高速に動作します。ですが、:only-*、:emptyは遅いです。
    • もしかすると、kQueryのチェックが甘いんだろうか?

uupaaは精度がメインで、基本的に速度を出すためにはキャッシュを有効活用するというスタイルで書けばいいのかと思いました。とはいえ、頻出しそうなセレクタは別処理が働くので、普通に使う分には世界最速で動作する気がします。

kQuery

より高い精度、より速い速度、より広い動作環境を目指して作られた自作のセレクターエンジンです。まだ、jQueryの独自セレクタを実装していないので、Sizzleと同等の規模になりそうですが、それでもSizzleよりも高い性能を示せそうです。

  • Unicode非対応
  • キャッシュ非搭載
    • 載せたら速くなるかもしれないけど、キャッシュ機構無くても別に速い。

さて、一応、jQueryの欠点を見たり、uupaaの高速化手法を取り入れたりした産物なので、特徴を。

  • *のタグ指定とタグ名の省略を同等に扱います。従って、.CLASSも*.CLASSも同じ処理、#IDも*#IDも同じ処理を行うようになっています。
    • *#IDでもgetElementByIdをするので、複数同じidが入っているようなものを検索する場合は、[id=ID]としなければなりません。
  • .CLASSはgetElemetsByClassNameとかXPathを使っているようなには負けますが、IE上では高速に探索できます。
  • 先頭に#IDが入る絞り込みは基本的に多分、高速です。
  • 子孫セレクタはやたら速いSizzleには勝てません。jQueryよりは速いです。
  • 属性セレクタも基本的に速いです。
  • :linkはjQueryの:inputみたいなもので、aタグ以外にもareaタグなどのリンクと見なされる要素にマッチします。:link,:visitedと同じ効能という具合でしょうか。
  • 疑似クラスはほどほどに速いです。
  • :notだけはuupaaを超えています。しかも、高速です。:not大好きです^^

正直、他のライブラリの欠点を見てから作ったので、高速だと思います。多分、素のuupaaよりも速いと思えたので、このくらいにします。

*1:行数を測るために、一応、正規化みたいな感じのことをしています。今回は/packer/を使って、コメントや改行、空白を除去した圧縮を一度行った後、JsDecoderで再度構築した際のソースの行数を測っています。測定箇所はセレクタのために使っている関数部分を適当に抜き出しています。uupaaの場合などはCore部分を測定に含めない感じです。まぁ、それぞれのソースの規模を把握するための比較用です。あんまり、厳密に考えないで下さい。