Personal tools
You are here: Home ブログ matsuyama Categories ajax
Document Actions

ajax

Up one level

Document Actions

Ajax 最適化 Tips - Prototype.js のパフォーマンス

prototype.js 1.6.0 RC で、カスタムイベントが使えるようになったり、ネイティブの forEach が使われるようになったり、簡単に DOM エレメントを構築できるようになったり、 isNumber で数値型かチェックできるようになったり、すばらしいバージョンアップであることは間違いないのですが、 prototype.js のソースを見てもわかるとおり、パフォーマンス云々よりも JavaScript 的にスマートな書き方・より単純で意味の取りやすい書き方が優先されており(もちろんこれは正しい方向)、あまり熟知せずにこれらの関数を乱用してしまうとアプリケーションのパフォーマンスに甚大な被害を与えてしまうことがあります。今回は prototype.js を使うにあたって(パフォーマンス的に)注意すべき点を紹介したいと思います。なお参考にする prototype.js のバージョンは 1.5.1 です。

Element.extend

Element.extend 自体それほど重い処理をやっているわけではないのですが、呼び出し関数が多いため自ずと慢性的なパフォーマンスの悪化をもたらします。しかし、 prototype.js ではエレメントであるようなオブジェクトに対しては即刻 $() を適用して間接的に Element.extend を実行しているので、 prototype.js を使う限りこの問題をさけることはできません(現在アリエルで開発しているプロダクトも Firebug でプロファイルを取ると必ず Element.extend が上位に食い込んでくるのです)。個人的には一番最適化してほしい関数です。ぱっと見るかぎりでは最適化の余地は十分ありそうですし。

Element.extend = function(element) {
  var F = Prototype.BrowserFeatures;
  if (!element || !element.tagName || element.nodeType == 3 ||
   element._extended || F.SpecificElementExtensions || element == window)
    return element;

  var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
   T = Element.Methods.ByTag;

  // extend methods for all tags (Safari doesn't need this)
  if (!F.ElementExtensions) {
    Object.extend(methods, Element.Methods),
    Object.extend(methods, Element.Methods.Simulated);
  }

  // extend methods for specific tags
  if (T[tagName]) Object.extend(methods, T[tagName]);

  for (var property in methods) {
    var value = methods[property];
    if (typeof value == 'function' && !(property in element))
      element[property] = cache.findOrStore(value);
  }

  element._extended = Prototype.emptyFunction;
  return element;
};

Array.from

var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0, length = iterable.length; i < length; i++)
      results.push(iterable[i]);
    return results;
  }
}

Array.from は Enumerable を配列にする関数ですが Array のメソッドを使いたいがためについつい以下のようなことをしてしまいます。

var a = $A(arguments).last();

これは本来

var a = Array.last.apply(arguments);

と書くべきです。一般的に破壊的な操作をしない限り Array.from を使うべきではありません。

var args = $A(arguments);
var a = args.shift();
return a.apply(this, args);

Array.without

個人的に prototype.js から一番外してほしい関数が Array.without です(関数型言語的に見れば良い関数なのですが)。

without: function() {
  var values = $A(arguments);
  return this.select(function(value) {
    return !values.include(value);
  });
},

外してほしい理由は主に Array.without が安易に使われてしまうからです。なので本質的には Array.without が悪いわけではありません。

僕が知らないだけなのかもしれませんが、 JavaScript では配列から特定の要素を削除する単純な手段がありません。

一般的な手段は、

var a = [1, 2, 3];
a.splice(a.indexOf(2), 1);

になるのですが、こんな気持ち悪いコード書けない、ということで Array.without を使ってしまうのです。 prototype.js 1.6.1 RC でもそれらしい関数がないですし、もしかしたら何かちゃんとした方法がすでにあって、ただそれを自分が知らないだけじゃないのかという不安にかられてしまうほど、 Array.remove なる関数が存在しないことに疑問を覚えてしまいます。

Element.update

update: function(element, html) {
  html = typeof html == 'undefined' ? '' : html.toString();
  $(element).innerHTML = html.stripScripts();
  setTimeout(function() {html.evalScripts()}, 10);
  return element;
},

エレメントの中身を書きかえようとして Element.update を使うのは好ましくありません。ソースを見てもわかるように、 script タグの削除・その script の評価(しかも setTimeout で)をやるからです。 Element.update は script タグが含まれる可能性がある場合のみに使うようにしてください。それ以外は普通に innerHTML への代入で問題ありません。

Element.getElementsBySelector

規模が大きいのでソースは示しませんが、 document.evaluate が使えない環境( IE6 など)で、巨大なエレメントに Element.getElementsBySelector を実行すると大変なことになります。ほとんどの場合、 document.getElementByIddocument.getElementsByClassName の組み合わせで書き換え可能です。

Element.up

up: function(element, expression, index) {
  element = $(element);
  if (arguments.length == 1) return $(element.parentNode);
  var ancestors = element.ancestors();
  return expression ? Selector.findElement(ancestors, expression, index) :
    ancestors[index || 0];
},

引数がない場合は問題ありませんが、引数がある場合は、全ての祖先ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、小規模関数の組み合わせで書き換え可能です。

Element.down

down: function(element, expression, index) {
  element = $(element);
  if (arguments.length == 1) return element.firstDescendant();
  var descendants = element.descendants();
  return expression ? Selector.findElement(descendants, expression, index) :
    descendants[index || 0];
},

引数がない場合は問題ありませんが、引数がある場合は、全ての子孫ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、 document.getElementByIddocument.getElementsByClassName の組み合わせで書き換え可能です。 Element.up よりこちらのほうが断然遅くなる可能性があるので気を付けてください。

Element.previous & Element.next

previous: function(element, expression, index) {
  element = $(element);
  if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
  var previousSiblings = element.previousSiblings();
  return expression ? Selector.findElement(previousSiblings, expression, index) :
    previousSiblings[index || 0];
},

next: function(element, expression, index) {
  element = $(element);
  if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
  var nextSiblings = element.nextSiblings();
  return expression ? Selector.findElement(nextSiblings, expression, index) :
    nextSiblings[index || 0];
},

Element.up とほとんど同じ。

余談

以上です。生半可なエントリで申し訳ないです。言うまでもありませんが、無駄な最適化はバグを生むだけです。まずは存分に prototype.js の機能を使って、それからプロファイルを取って適切にボトルネックを取り除きましょう。ただ Element.getElementsBySelector なんかは最初から使うべきでないぐらいの代物ですが。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/ajax-tips-prototype-js/tbping

Ajax 最適化 Tips : 要素をカスタマイズする理想的な方法

HTML 内で要素をカスタマイズするための書き方はろいろありますが、おそらく理想的な方法は以下のようになるでしょう。

foo.js:

function init(element) {
  $(element).observe('click', function() { alert('Clicked!') });
}

foo.jsp:

<div onelementready="init(this)">
  Custom Element
</div>

しかし onelementready などというイベントは存在しないので、

foo.jsp:

<div id="element1">
  Custom Element
</div>
<script type="text/javascript">
  init($('element1'));
</script>

とやったり、

foo.jsp:

<div id="element1">
  Custom Element
</div>
<script type="text/javascript">
  init(document.scripts[document.scripts.length - 1].previousSibling);
</script>

とやったり、

foo.jsp:

<div id="element1" onmouseenter="init(this)"> <!-- 遅延させる -->
  Custom Element
</div>

とやったりしますが、それぞれ一長一短があり、どれかを一般的な手法として用いることはできません。

そこで上記の理想を実現してみました ( 実用できるレベルではありません )。ソースコードは

http://code.google.com/p/elementevent/

から取得できます。

IE の Behavior と Firefox の XBL という機能を使って無理矢理実現しています。言うまでもありませんが、 Opera や Safari では動きません。

これを使えば、要素生成時と要素破棄時のイベントをハンドリングすることができるようになります ( 正確にはスタイルがアタッチされたときとデタッチされたとき ) 。 要素生成時は onelementready で、要素破棄時には onelementdispose でハンドリングします。なお、パフォーマンスの劣化を防ぐために elementevent クラスがある要素のみ利用可能になっています。

<div class="elementevent"
     onelementready="alert('Element is ready.')"
     onelementdispose="alert('Element is disposed.')" />

属性が指定されていれば、どのような要素の生成のされかたでも正しくハンドリングできます。以下のような HTML を書いて、

<html>
  <head>
    <link rel="stylesheet" href="elementevent.css" type="text/css" />
    <title>elementevent test</title>
    <script type="text/javascript">
      function ok(text) {
        var test = document.createElement('div');
        test.innerHTML = text + ' : <i>ok</i><br />';
        document.body.appendChild(test);
      }
    </script>
  </head>
  <body>
    <div class="elementevent"
         onelementready="ok('onelementready on static element')"
         onelementdipose="ok('onelementdispose on static element')">
      <span class="elementevent"
            onelementready="ok('onelementready on inner static element')"
            onelementdipose="ok('onelementdispose on inner static element')">
      </span>
    </div>
   
    <script text="text/javascript">
      var element = document.createElement('div');
      element.className = 'elementevent';
      element.setAttribute('onelementready', "ok('onelementready on dynamic element')");
      element.setAttribute('onelementdispose', "ok('onelementdispose on dynamic element')");
      document.body.appendChild(element);

      var html = '<span class="elementevent" ' +
                   'onelementready="ok(\'onelementready on element generated by innerHTML\')" ' +
                   'onelementdispose="ok(\'onelementdispose on element generated by innerHTML\')"></span>';
      element.innerHTML = html;
    </script>
  </body>
</html>

ブラウザで読み込ませると、

onelementready on static element : ok
onelementready on inner static element : ok
onelementready on dynamic element : ok
onelementready on element generated by innerHTML : ok

と表示されます。

注目すべきは従来の方法ではどうしても必要だった script タグが完全に除去されている点です。また、これを用いれば HTML に不必要なロジックを埋め込まざるを得ない状況も回避できるようになります ( ロジックの分離 ) 。実用できるレベルであれば夢のような機能ですが、対応ブラウザが少ないのと、イベントの発火タイミングがかなり曖昧なのとで、なかなか実用できないでいる次第です。

まあ、パフォーマンス上の懸念がなければ従来の方法でも十分だと言えばそうなのですが。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/custom-element-initialization/tbping

Re:Ajax 最適化 Tips : 要素をカスタマイズする理想的な方法

Posted by Anonymous User at 2007-10-19 23:07
XBL ってこんなことできるんですか。すごいですね。
DOM の仕様には mutation event というのがあるので、
それに名前を揃えておくとおしゃれかも知れません。
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MutationEvent
衝突を避けるために違う名前にしておくのも、
それはそれでアリだとは思います。

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.