sitepoint から「本当にjQueryが必要ですか?」とタイトルのついた3本の記事を紹介します。
- Do You Really Need jQuery?
- Native JavaScript Equivalents of jQuery Methods: the DOM and Forms
- Native JavaScript Equivalents of jQuery Methods: Events, Ajax and Utilities
言うまでもなく著者の Craig Buckler さん の趣旨は、「jQueryを使うのは止めよう」ではありません。ネイティブ関数で代替えできるのは、古い IE のサポートが必要なく、ごく簡単なケースに限られます。その代わりに得るものは「速さ」です。そこで、どの程度「速い」のかを所々 jsperf の結果で補ってみたいと思います。
また JavaScript 初学者の人も、jQuery が内部でどう苦労しているのか知るのも良いと思います。私自身の勉強も兼ね、その他の情報も色々と補っていますので、良かったら見ていって下さい 。
jQueryが必要な理由
まずは1本目の「Do You Really Need jQuery?」から、導入部分です。著者は以下の6つを挙げています。
- 同じ HTML でも、ドキュメントツリーの構造は各ブラウザによって異なります。そのため、DOM の巡回と操作に関して、ブラウザ間の相違に注意しなければなりません。例えば Firefox では、スペースや改行の
ホワイトスペースノード
を(正しく)DOM 中に構築しますが、IE6 はそうではありません。このためnode.firstChild
が返すノードが同じであると仮定することは出来ません。jQuery はこういったブラウザ間の違いを吸収してくれます。 - Ajax は多くのブラウザでネイティブにサポートされていますが、マイクロソフトが XMLHttpRequest を最初に実装したにもかかわらず、IE では ActiveX のコントロールが使われます。
- IE のイベントモデルは他のブラウザと異なりますし、またそういったブラウザ間でも互いに矛盾する点があったりします。
- CSS2.1 のサポート状況が大きく異なります。
- アニメーションは、各ブラウザの異なるボックスモデルやフォームのコントロール、iframe(IE6 は、セレクトボックスと iframe が
z-index
を無視する ため、ページ中のどの要素よりも一番上になります)を扱う時に、特に難しくなります。 - Firebug や Firefox のエラーコンソールが出現する以前は、開発ツールが貧弱でした。IE でエラーをモーダルボックスで表示させるのが、ブラウザに唯一ビルトインされたツールだったのです。
最後の 6. は「今さら既知の問題でつまずいて時間をムダにすることはないよネ」ということだと思います。
いずれにしても著者は jQuery を 暖かい毛布
や ライフジャケット
に例えていて、こういったクロスブラウザの抱える問題点を知らなくても、快適に使えるメリットは認めています。
一方「必要のない理由」として、使わない機能(jQuery 1.8 からはカスタムビルドが可能 なようですが)で肥大化したファイルを読み込むのがムダ、所詮 jQuery も JavaScript で書かれているのだから代替えは可能、というのが著者の趣旨です。
これに対し 82 のコメント(既にクローズされてます)が寄せられていて、賛否で言えば「否」が多数なのですが、次稿以降で具体的なコードや高速化の数値が出て来ると「賛」が多数派になります 。
DOM セレクタ
jQuery では、CSS のセレクタを使って DOM ノードの検索を次のようにします。
var n = $("article#first p.summary");
同じことをネイティブ関数では次のようにします。
var n = document.querySelectorAll("article#first p.summary");
document.querySelectorAll
は、すべてのモダンブラウザと IE8(CSS2.1 のセレクタまでですが)で使えます。jQuery ではさらに複雑なセレクタも使えるよう追加されたコードもあるのですが、大部分は $()
の内部でラップされた document.querySelectorAll
が働きます。
類似のネイティブ関数には次の様なものがあります。
document.querySelector(selector)
− セレクタに合致する最初のノードだけを抽出します。IE8 以上のブラウザで動作 します。document.getElementById(idname)
− ID 名で定まるただ1つのノードを抽出します。document.getElementsByTagName(tagname)
− タグ名にマッチする要素のノード集合を抽出します。document.getElementsByClassName(class)
− 特定のクラスが指定された要素のノード集合を抽出します。IE9 以上のブラウザで動作 します。
「ノードの集合」は NodeList
オブジェクト(IE8 では HTMLCollection
が、W3C のワーキングドラフト では classList
)で、単なる配列ではありません。これについては forEach
の項で解説します。
さて元記事では、これらの代替えを含めた速度比較が紹介されています。
code | time |
---|---|
// jQuery 2.0 var c = $("#comments .comment"); |
4,649 ms |
// jQuery 2.0 var c = $(".comment"); |
3,437 ms |
// querySelectorAll var c = document.querySelectorAll("#comments .comment"); |
1,362 ms |
// querySelectorAll var c = document.querySelectorAll(".comment"); |
1,168 ms |
// getElementById / getElementsByClassName var n = document.getElementById("comments"); var c = n.getElementsByClassName("comment"); |
107 ms |
// getElementsByClassName var c = document.getElementsByClassName("comment"); |
75 ms |
計測条件が明らかでなく、また著者によれば実世界を反映したものではないとのことですが(おそらく1000とか1万とか繰り返して計測したものと思われ)、最大で60倍もの開きがあります。この手の「誰もが疑問に思う速度比較」は、jsperf を探せば出てきます。ここでは次の2つを参考に挙げておきます。
- jQuery 2 vs. document.querySelector – Firefox 24 でネイティブの方が4倍ほど高速です。Revision23 では、jQuery とネイティブ関数のハイブリッドも計測されています。
- getElementById vs. getElementsByClassName vs. querySelector vs. jQuery vs xpath – jQuery 1.x ですが、ネイティブ関数や jQuery との組み合わせを含め、すべてが比較されているリビジョンです。Chrome 29 で計ったところ、
document.getElementsByClassName
が$()
の71倍という結果が出ました。
まぁ、この辺りの結果 を見れば、Android の 2.3 とか 4.0 とかでも、1回当たりの実行時間はナノ秒のオーダーなので、繰り返しが少なければ問題にはならない、という程度でしょう。
参考記事
DOM の操作
私も最近よく使う innerHTML
です。
$("#container").append("<p>more content</p>");
document.getElementById("container").innerHTML += "<p>more content</p>";
記事ではさらに次のコードが “速い” とされています。
var p = document.createElement("p"); p.appendChild(document.createTextNode("more content"); document.getElementById("container").appendChild(p);
むしろ twitter タイムラインのように、リストに複数の要素を追加する場合の方がより現実的で、その意味では、createElement
、createDocumentFragment
、cloneNode
等が詳しく解説されている次の記事の方が優れています。
- DOM操作の最適化によるJavaScriptチューニング(前編) | HTML5Experts.jp
- DOM操作の最適化によるJavaScriptチューニング(後編) | HTML5Experts.jp
また速度の比較は、jsperf を検索 すれば 山ほど出てきます (誰か jsperf の HTML5Experts.jp バージョンを作ってくれませんかね?)。
上記の記事では IE、Firefox は
と言及されていますし、jsperf の結果も大方その通りです。しかし fragment vs. appendChild vs. innerHTML (8) とか innerHTML vs jQuery vs createElement() and appendChild() (4) を見ると、モバイル系では DOM 操作系の方が速そうです。もっとも realistic innerHTML vs. appendChild vs jQuery.append() (15) では、サンプル数が少ないながら Chrome Mobile や Mobile Safari の最新バージョンでその傾向が逆転しそうな気配なので、悩ましいところです。
innerHTML
の方が若干速く、Chrome、Safari は DOM 操作メソッドの方が若干速くなっています
さて次は、子ノードを空にするパターンです。
$("#container").empty();
document.getElementById("container").innerHTML = null;
あるいは
var c = document.getElementById("container"); while (c.lastChild) c.removeChild(c.lastChild);
そして最後は、あるノード以下すべてを削除するパターンです。
$("#container").remove();
あるいは
var c = document.getElementById("container"); c.parentNode.removeChild(c);
また速度面だけではなく、セキュリティにも気をつけるベキですネ。
参考記事
スクリプトの起動
<script>
を <head>
内に置くのが常識だった昔は、関数 start()
を起動するのに DOM ready を待って $(start)
(無名関数 を使えば $(function () {...})
です) としていました。
がページ速度向上のベストプラクティスとして認知された現在では、例えば「Google Analytics でページが完全に表示される前に発生したユーザーイベントを逃したくない」とか、「画像の遅延読み込みをするために HTTP リクエストが発行される前に DOM を書き換えなきゃイケナイ」とかの理由がない限り、<script>
は </body>
直前におくべし</body>
直前の <script>
で普通に起動すれば良いのです。
私は最近もっぱらこのパターンです。で、この話題についてはちょっと別な視点も交え、後日あらためて掘り下げてみるつもりです。
2013年9月28日 追記
クロスブラウザな DOM ready のコードは掃いて捨てるほど存在します。どうしてもというなら、以下がオススメ。
- jquery.documentReady.js − 著名な Addy Osmani さん のコードで、jQuery 1.8.3 を元に単体で使えるよう加工されたものです。MDN でも紹介されてます。
- ded/domready − JavaScript のパッケージ管理フレームワーク Ender.js が作者。ブラウザの互換性も検証されていて、安心です。縮小版のサイズは 0.751KB。これ以外にも CSS セレクタ・エンジンの qwery や Ajax ライブラリ reqwest などがあり、jQuery 対応する以前の Octopress にも採用されていました。
- tubalmartin/ondomready − こちらも jQuery が元で、jQuery 1.10.1 にまで追従しています(jQuery 1.8.1 からの変化点は IE 対策?)。縮小版のサイズは 0.903KB。
forEach
jQuery には each
という便利なユーティリティ関数があります。
$("p").each(function(i) { console.log("index " + i + ": " + this); });
この each
は jQuery 内部のメソッドチェーンでも使われています。
$("p").addClass("newclass").css({color:"#f00"});
ネイティブな関数では、IE9 以上を含む多くのブラウザでサポートされている Array.forEach
がありますが、配列にしか適用できないので、NodeList
オブジェクトに適用するには、Array.slice
が 配列の様なオブジェクト
を配列に変換するという機能を用いて、次のようにする事ができます。
Array.prototype.slice.call(document.getElementsByTagName("p")).forEach(function (obj, i) { obj.className += " newclass"; obj.style.color = "#f00"; });
element.className
は DOM Level 2 で定められていて、スペースで区切られた要素のクラス文字列
です。
ただし上のコードはちょっと長いので、著者は、オブジェクトでも配列でも機能するユーティリティ関数 Each
を作るのがオススメだとしています。
function Each(obj, fn) { if (obj.length) for (var i = 0, ol = obj.length, v = obj[0]; i < ol && fn(v, i) !== false; v = obj[++i]); else for (var p in obj) if (fn(obj[p], p) === false) break; }
イベント
著者によれば、jQuery 以前のイベントの扱いはひどいもので、IE6、7、8 はそれぞれ異なるイベントモデルを持っていた上、W3C 準拠とは言えないほどの不整合があったそうです。jQuery は、そういったイベントを扱い易くしました。
$("#clickme").on("click", function(e) { console.log("you clicked " + e.target); e.preventDefault(); });
IE9 以上であれば、ネイティブ関数でもそれほど難しくはありません。
document.getElementById("clickme").addEventListener("click", function(e) { console.log("you clicked " + e.target); e.preventDefault(); });
IE6、7、8 でも動作させたいなら、次の様な DOM Level 1 の on
メソッドが使えます。
document.getElementById("clickme").onclick = function(e) { e = e ? e : window.event; // IE6/7/8 用 console.log("you clicked " + e.target); e.preventDefault(); };
jQuery のカスタム・イベントをマネたい時は、Remy Sharp(jsbin の作者)の remy/min.js がオススメです。$
セレクタやチェーン可能な on
と trigger
、forEach
がサポートされていて、サイズはたったの 609 バイトしかありません!
Ajax
最後は Ajax です。jQuery は、XMLHttpRequest だけでなく、動的なスクリプトの挿入($.getScript
)、GET リクエストや POST 送信等に加え、JSON のハンドリングなど、およそあらゆる状況に柔軟に対応できる機能を持っています。が、これらをすべて使うことは滅多にないでしょう。
典型的な Ajax のコードは、以下の様なものです。
$.ajax({ url: "webservice", type: "POST", data: "a=1&b=2&c=3", success: function(d) { console.log(d); } });
これは次のように置き換えることが出来ます。
var r = new XMLHttpRequest(); r.open("POST", "webservice", true); r.onreadystatechange = function () { if (r.readyState != 4 || r.status != 200) return; console.log(r.responseText); }; r.send("a=1&b=2&c=3");
エラー処理やタイムアウト処理が必要なら、MDN の この記事 や stackoverflow の この記事 を参考にすれば良いでしょう。
また jsonp 限定なら Simple Ajax only for JSONP はいかがでしょう。動的に <script>
を生成しているので、eval()
とかしなくても済みます(サンプル)。
さらには、バイナリーデータやアップロード、CORS での通信をしたかったら XMLHttpRequest2 をネイティブに使えば良いでしょう。それには HTML5 Rocks のチュートリアル がとても参考になります。
最後に
著者の Buckler さんは最後に Vanilla JS のリンクを紹介しています。これは “ネイティブが一番速くて軽いよ というジョーク・サイトなんですが、ネイティブが如何に速いか、参考になるでしょう。
まぁ、私が作る程度のものなら、あまりパフォーマンス的なことは気にしなくてもいいかなってことも分かりましたが、それでも常にベストなコードは書きたいナと思います。この記事の紹介で、そういう勉強にはなりました 。
これは個人的見解ですが、エンジニアには、およそその特性を表す2つの軸があって、1つは物事を源流からきちっと押さえていこうとする特性、もう1つは、より上位の付加価値の高さを求める特性です。そして常々その両方を持ち合わせたいナと思っています。この記事はそんな気持ちから取り上げた次第です。
参考記事
追記
ネイティブ関数の可読性と抽象度の低さを挙げる twitter コメントを頂いています。なるほど、jQuery は、$()
という 高級言語っぽい
ラッパーI/Fを世に広げた功績もあるのだと気付かさるコメントです。
元記事には、次のような工夫のコメントが書き込まれていますので、参考までに追記しておきます。document...
をずらずらと書き並べるよりは、少しマシになると思います。
/*! Salt.js DOM Selector Lib. By @james2doyle */ window.$ = function(selector) { // an object containing the matching keys and native get commands var matches = { '#': 'getElementById', '.': 'getElementsByClassName', '@': 'getElementsByName', '=': 'getElementsByTagName', '*': 'querySelectorAll' }[selector[0]]; // you can treat a string as an array of characters // now pass the selector without the key/first character var el = (document[matches](selector.slice(1))); // if there is one element than return the 0 element return ((el.length < 2) ? el[0]: el); };
使い方は、james2doyle/saltjs を参照ください。
ピンバック: 【コンピューター】 ビル・ゲイツ氏、「Ctrl+Alt+Del」は失敗だったと認める – ITmedia ニュース 2013年09月27日 夕刊 | aquadrops * news
ピンバック: 【コンピューター】 ChangeLog を支える英語 2013年09月28日 深夜便 | aquadrops * news
良い記事ですね。
「代替え」ではなく「代替(だいたい)」では?
お恥ずかしい。公開当時から他の方からもご指摘をもらってまして、タイトルを修正しました。ご指摘、ありがとうございました。