performance tuning
Up one levelAjax 最適化 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
Ajax 最適化 Tips - getElementById する前に
getElementById で要素を手っ取り早く取得するのはとても良いイディオムですが(コードが短かくなる)、パフォーマンスのことを心配する場合は以下のことを念頭に入れておくと良いかもしれません。
Microsoft の DHTML Collections(*) によると、
(*) http://msdn2.microsoft.com/en-us/library/ms533048.aspx
document 以下の特定の種類の要素をフラットにアクセスできるコレクションという形で提供しています。通常ノードツリーをトラバースするよりコレクションにアクセスするほうが速いので、これを使わない手はありません。特に便利だと思われるのは以下のようなものでしょうか(他にも便利なものがありますが、今回は document に対してのコレクションのみに限定したいので省きました)。
- document.anchors
- document.forms
仮に以下のような HTML があるとします。
<a id="button1">Click me!</a> <form id="form1"> ... </form>
この場合、
document.getElementById('button1').innerHTML = 'Do not click me!'; document.getElementById('form1').onsubmit = function() { alert('Submit!') };
と書くより、
document.anchors['button1'].innerHTML = 'Do not click me!'; document.forms['form1'].onsubmit = function() { alert('Sumit!') };
と書くほうが格段に速いです。ただコレクションのインデックスは整数値あるいは id/name を受けつける寛大な仕様になっているので、もう少し厳密にするなら以下のように書くと良いかもしれません。
function getElementByIdWithHint(id, expectedTagName) { expectedTagName = (expectedTagName || '').toLowerCase(); var collection; if (expectedTagName == 'a') collection = document.anchors; else if (expectedTagName == 'form') collection = document.forms; if (collection) { for (var i = 0, len = collection.length; i < len; i++) { var elem = collection[i]; if (elem.id == id) return elem; } return null; } else return document.getElementById(id); } getElementByIdWithHint('button1', 'a').innerHTML = 'Do not click me!'; getElementByIdWithHint('form1', 'form').onsubmit = function() { alert('Sumit!') };
言うまでもありませんが、 getElementById を使わないにこしたことはありません。ほとんどの場合はうまくキャッシュさせたり、もっとオーダーの低い firstChild なりを使って何とかなったりします。その辺りのこともいずれ話したいと思います。
- Category(s)
- javascript
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/before-getElementById/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 に不必要なロジックを埋め込まざるを得ない状況も回避できるようになります ( ロジックの分離 ) 。実用できるレベルであれば夢のような機能ですが、対応ブラウザが少ないのと、イベントの発火タイミングがかなり曖昧なのとで、なかなか実用できないでいる次第です。
まあ、パフォーマンス上の懸念がなければ従来の方法でも十分だと言えばそうなのですが。
- Category(s)
- ajax
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/custom-element-initialization/tbping
Re:Ajax 最適化 Tips : 要素をカスタマイズする理想的な方法
DOM の仕様には mutation event というのがあるので、
それに名前を揃えておくとおしゃれかも知れません。
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MutationEvent
衝突を避けるために違う名前にしておくのも、
それはそれでアリだとは思います。