以下斜め読んだ内容

pseudo translation of useful posts, book reviews, remarks,etc. twitter: feeddict

Siddharth「jQuery初心者へ贈る、あなたのコードをテストしながらより良くするやり方」

Nettuts+の2010.4.29のチュートリアル記事

How jQuery Beginners can Test and Improve their Code

既出tips多いが啓蒙的

以下斜め読んだ内容
  • jQuery以後jsのコード書くが快適になった
  • コードを少し改変しただけで、コードはより読みやすくなり、速度も劇的に良くなる
  • コードをより良くしていくためのTIPSを何個か紹介
  • テストするための環境作り
    • Firefox+Firebugを前提にできるなら、以下のtest用htmlコピペ
<!DOCTYPE html>
<html lang="en-GB">

<head>
<title>Testing out performance enhancements - Siddharth/NetTuts+</title>
</head>

<body>

<div id="container">
<div class="block">
<p id="first">
  Some text here
</p>
<ul id="someList">
  <li class="item"></li>
  <li class="item selected" id="mainItem">Oh, hello there!</li>
  <li class="item"></li>
  <li class="item"></li>
</ul>
</div>
</div>

<script  src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script>
  console.profile() ;//計測開始

  // Our code here

  console.profileEnd();//計測終了
</script>

</body>
</html>
  • 上のtest用htmlのscript要素部分
    • 「console.profile()」と「console.profileEnd()」でテストしたいコードの実行時間を計測する
    • 「console.profile()」と「console.profileEnd()」の間にテストしたいコード書く。
    • 自分は普段はprofile()とprofileEnd()使ってるけど、
      • テストしながら改善していくということがどういうことが知ってもらうにはconsole.profile()でも示せる

tips1:要素の存在をチェックする

    • ありがちケース。サイト全体でjsファイル1個を共通で使っている。
      • このケースだと、存在しない要素に対して処理が実行されてることがよくある。
      • 普通だとエラー吐かれるがjQueryはうまく立ち回ってくれる。でもこれは気にしなくていいということではない。
      • 取得しようとしてる要素が存在してるのかはキチンと把握すべき。
    • ベストプラクティス
      • 処理を適用したい要素が存在するページに対してだけ、処理が実行されるようにしておく
        • あらゆる処理に対して存在をチェックするというのは止めておく。効果が上がる場所が使い所
      • とりあえず全部の処理をDOMツリー構築後に実行しておくという真似は避ける
//before:関数コール回数=21、実行速度0.477ms
console.profile();
var ele = $("#somethingThatisNotHere");
ele.text("Some text").slideUp(300).addClass("editing");
$("#mainItem");
console.profileEnd();
//after:関数コール回数=4、実行速度0.112ms
console.profile() ;
var ele = $("#somethingThatisNotHere");
if ( ele[0] ) {//処理を適用したい要素がページ内にあるのかチェック
   ele.text("Some text").slideUp(300).addClass("editing");
}
$("#mainItem");
console.profileEnd();

tips2:セレクタをうまく使う

  • セレクタにclassを使うとき、idにできないか検討するのが大事
    • idを渡すと、ネイティブで高速なgetElementByIdが内部的に使われるが、classを渡すと内部的には色々大変な処理が必要。
      • getElementsByClassNameがモダンブラウザでは軒並み実装されてるがieなど古いブラウザでは未実装なので。
  • セレクタにclassしか使えない場面でも要素セレクタと組み合わせれないか検討すべし
    • 例。「.selected」よりも「li.selected」の方がよい
        • チェックしないといけない要素がページ内の全ての要素からli要素だけに限定されるから
    • 具体例(↓)では差はごく僅か。これはFirefoxだから。IEだともっと差は大きくなる。
//具体例で検証
//classだけの場合。実行速度=0.308ms
console.profile() ;
$(".selected");
console.profileEnd();
//要素セレクタ+classの場合。実行速度=0.291ms
console.profile() ;
$("li.selected");
console.profileEnd();
//idを追加してidとclassの子孫セレクタの場合。実行速度=0.283ms
console.profile() ;
$("li.selected");
 console.profileEnd();
//idと要素セレクタとclassの子孫セレクタの場合。実行速度=0.275ms
console.profile() ;
$("li.selected");
 console.profileEnd();
//一応、idだけのセレクタも比較する。実行速度=0.165ms
console.profile() ;
$("li.selected");
 console.profileEnd();

tips3:Sizzle(jQueryセレクタエンジン)の仕組みを把握。context(セレクタapiの第二引数)使う

  • sizzleでは、CSSセレクタは「右側から左側へ」順番に解釈される
    • 「$("#someList .selected");」を例に説明
      • 全ての要素が取得
      • 次に、selectedというclassを持たない要素を除外
      • 次に、selectedというclassを持つ全ての要素からid「someList」が祖先である要素のみを抽出
  • sizzleのパースの仕組みを念頭におけば、セレクターの一番右端の箇所は出来るだけユニークなものにすべし、となる
    • li.selectedの方が.selectedよりもチェックしないといけない要素の範囲が小さくなるのでよい。
  • contextを使うと、セレクターエンジンがチェックする範囲を制限できる。
//補足。contextは特定の要素でなければいけないので「$('#someList')[0]」となってる。
var someList = $('#someList')[0];
$(".selected", someList);
//上記だと、$('#someList')[0]の中の要素の中に制限される。
  • contextでパフォーマンスがアップしない場合は、find()を使うとスピードアップする、かもしれない
$('#someList').find('.selected');

tips4:selector api使ったクエリーは無駄なく使いまわす。

// ダメなケース。3回も同じ要素を取得してる
//処理時間=1.233ms。呼び出し回数=30
 console.profile() ;
 $("#mainItem").hide();
 $("#mainItem").val("Hello");
 $("#mainItem").html("Oh, hey there!");
 $("#mainItem").show();
 console.profileEnd();
//よいケース。メソッドチェーンで使いまわし。
//処理時間=1.009ms。呼び出し回数=24
console.profile();
$("#mainItem").hide().val("Hello").html("Oh, hey there!").show();
console.profileEnd();
//メソッドチェーンが使えないケースなら、取得した要素をキャッシュしておく
//処理時間・呼び出し回数は上と同じ。
console.profile() ;
var elem = $("#mainItem");
elem.hide();
//Some code
elem.val("Hello");
//More code
elem.html("Oh, hey there!");
//Even more code
elem.show();
console.profileEnd();

tips5:DOM操作の回数は出来るだけ減らす

  • オンザフライで生成させるたびにdocumentに追加するろ再フローがそのつど発生
    • これはロス
  • オンザフライでの要素の生成が終わった段階で、まとめてdocumentへappendする
    • こうすれば再フロー回数は1回に収まる。
//いちいちappendしたとき。
//処理時間=17.851ms。呼び出し回数=602回
console.profile() ;
var list = $("#someList");
for (var i=0; i<50; i++)

   list.append('<li>Item #' + i + '</li>');
}
console.profileEnd();

//まとめてappendしたとき
//処理時間=5.29ms。呼び出し回数=210回
console.profile() ;
var list = $("#someList");
var items = "";
for (var i=0; i<50; i++){
     items += '<li>Item #' + i + '</li>';
 }

list.append(items);
console.profileEnd();
//脱jQueryして、document.getElementById使うともっと早くなる
//処理時間=3.644ms。呼び出し回数=5回
//汎用性が下がるが

注意点

  • ここまでに比較したテストケースでは、最適化前/後の差は数ミリセカンドレベル
    • test用htmlのDOMツリーが小規模だから。
    • 実務レベルのサイト/ウェブアプリになれば、数千ノードとか軽くいくので、最適化によって節約できる時間はどんどん増える。
  • 今回の記事では、セレクタapi周りだけ。取得したjQueryオブジェクトに処理を加えていく中での最適化はトピック外。
  • 一般化できる話は少ないが、最適化をしていく感覚・雰囲気を掴むにはいい例だと思う。
  • サーバーの接続時間とレスポンスタイムの最適化がウェブアプリの最適化の主戦場だけど、コードのリファクタリングによる最適化も重要であることには変わりない。