そのコード、本当にjQueryが必要ですか?ネイティブ関数の代替Tips集

sitepoint から「本当にjQueryが必要ですか?」とタイトルのついた3本の記事を紹介します。

言うまでもなく著者の Craig Buckler さん の趣旨は、「jQueryを使うのは止めよう」ではありません。ネイティブ関数で代替えできるのは、古い IE のサポートが必要なく、ごく簡単なケースに限られます。その代わりに得るものは「速さ」です。そこで、どの程度「速い」のかを所々 jsperf の結果で補ってみたいと思います。

また JavaScript 初学者の人も、jQuery が内部でどう苦労しているのか知るのも良いと思います。私自身の勉強も兼ね、その他の情報も色々と補っていますので、良かったら見ていって下さい :D

jQueryが必要な理由

まずは1本目の「Do You Really Need jQuery?」から、導入部分です。著者は以下の6つを挙げています。

  1. 同じ HTML でも、ドキュメントツリーの構造は各ブラウザによって異なります。そのため、DOM の巡回と操作に関して、ブラウザ間の相違に注意しなければなりません。例えば Firefox では、スペースや改行の ホワイトスペースノード を(正しく)DOM 中に構築しますが、IE6 はそうではありません。このため node.firstChild が返すノードが同じであると仮定することは出来ません。jQuery はこういったブラウザ間の違いを吸収してくれます。
  2. Ajax は多くのブラウザでネイティブにサポートされていますが、マイクロソフトが XMLHttpRequest を最初に実装したにもかかわらず、IE では ActiveX のコントロールが使われます。
  3. IE のイベントモデルは他のブラウザと異なりますし、またそういったブラウザ間でも互いに矛盾する点があったりします。
  4. CSS2.1 のサポート状況が大きく異なります。
  5. アニメーションは、各ブラウザの異なるボックスモデルやフォームのコントロール、iframe(IE6 は、セレクトボックスと iframe が z-index を無視する ため、ページ中のどの要素よりも一番上になります)を扱う時に、特に難しくなります。
  6. 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 が働きます。

類似のネイティブ関数には次の様なものがあります。

  1. document.querySelector(selector) − セレクタに合致する最初のノードだけを抽出します。IE8 以上のブラウザで動作 します。
  2. document.getElementById(idname) − ID 名で定まるただ1つのノードを抽出します。
  3. document.getElementsByTagName(tagname) − タグ名にマッチする要素のノード集合を抽出します。
  4. 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つを参考に挙げておきます。

まぁ、この辺りの結果 を見れば、Android の 2.3 とか 4.0 とかでも、1回当たりの実行時間はナノ秒のオーダーなので、繰り返しが少なければ問題にはならない、という程度でしょう。

Androidでの実行時間

Androidでの実行時間

参考記事

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 タイムラインのように、リストに複数の要素を追加する場合の方がより現実的で、その意味では、createElementcreateDocumentFragmentcloneNode 等が詳しく解説されている次の記事の方が優れています。

また速度の比較は、jsperf を検索 すれば 山ほど出てきます ;-) (誰か jsperf の HTML5Experts.jp バージョンを作ってくれませんかね?)。

上記の記事では IE、Firefox は innerHTML の方が若干速く、Chrome、Safari は DOM 操作メソッドの方が若干速くなっています と言及されていますし、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 の最新バージョンでその傾向が逆転しそうな気配なので、悩ましいところです。

さて次は、子ノードを空にするパターンです。

$("#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 () {...}) です) としていました。

<script></body> 直前におくべし がページ速度向上のベストプラクティスとして認知された現在では、例えば「Google Analytics でページが完全に表示される前に発生したユーザーイベントを逃したくない」とか、「画像の遅延読み込みをするために HTTP リクエストが発行される前に DOM を書き換えなきゃイケナイ」とかの理由がない限り、</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.classNameDOM 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 Sharpjsbin の作者)の remy/min.js がオススメです。$ セレクタやチェーン可能な ontriggerforEach がサポートされていて、サイズはたったの 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 のリンクを紹介しています。これは “ネイティブが一番速くて軽いよ というジョーク・サイトなんですが、ネイティブが如何に速いか、参考になるでしょう。

まぁ、私が作る程度のものなら、あまりパフォーマンス的なことは気にしなくてもいいかなってことも分かりましたが、それでも常にベストなコードは書きたいナと思います。この記事の紹介で、そういう勉強にはなりました :D

これは個人的見解ですが、エンジニアには、およそその特性を表す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 を参照ください。

そのコード、本当にjQueryが必要ですか?ネイティブ関数の代替Tips集」への4件のフィードバック

  1. ピンバック: 【コンピューター】 ビル・ゲイツ氏、「Ctrl+Alt+Del」は失敗だったと認める – ITmedia ニュース 2013年09月27日 夕刊 | aquadrops * news

  2. ピンバック: 【コンピューター】 ChangeLog を支える英語 2013年09月28日 深夜便 | aquadrops * news

    1. tokkonoPapa 投稿作成者

      お恥ずかしい。公開当時から他の方からもご指摘をもらってまして、タイトルを修正しました。ご指摘、ありがとうございました。

      返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

海外からの投稿は受け付けていません