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.getElementById と document.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.getElementById と document.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 なんかは最初から使うべきでないぐらいの代物ですが。
- Category(s)
- program
- javascript
- ajax
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/ajax-tips-prototype-js/tbping