読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

JavaScriptパフォーマンスベストプラクティスその8

Nokia Developerより

イベント

イベント委譲を使うこと
  • もしイベントハンドラをバインドする必要がある新しい要素を動的にたくさん生成するなら、イベントハンドラを個々のオブジェクトに割り当てることは、すぐにことが大きくなって、高価になる。
  • 複数イベントハンドラ(例えば、1つノ要素にclickとblurを。100要素の場合、これは200イベントハンドラを意味する)を割り当てるなら、特にこれは興味深い。
  • DOMレベル2イベントモデルを使って、すべてのイベントは階層の最も上にあるdocumentオブジェクトに向かって伝搬する。これは、コントローラを呼び出し、イベントオブジェクトをそれに渡すイベントハンドラをdocumentにバインド可能であることを意味する。コントローラは責任を持ってイベントの内部をインスペクトし、適切なロジックに送る。
  • Assigning event handlers to individual objects can add up quickly and is expensive if you create a lots of new elements dynamically to which event handlers need to be bound.
  • This becomes especially interesting if you assign multiple event listeners (e.g. click and blur to a one element. In case of 100 elements that would mean 200 event handlers.
  • Using DOM Level 2 event model all events propagate toward the document object which is highest up in the hierarchy. This means that one can bind event listeners to document which invokes a controller and passes the event object to it. The controller is responsible for inspecting the internals of the event and dispatching to appropriate logic.

遅い:

var elems = [first, ..., last]; // an array which holds say 1000 references to element to which assign the event handlers to
for (var i, l = elems.length; i++; i < l) {
    elems[i].onclick = function() {};
    elems[i].onblur = function() {};
}

速い:

//HTML
<button id="doSomething">Click me to do something</button> // you can add more of elements without the need to worry about binding event handlers

>|javascript|
// JS
document.addEventListener('click', function(event) { eventController(event); }, false);
document.addEventListener('blue', function(event) { eventController(event); }, false);

function eventController(event) {
// inspect the event object internals and do something wise
if (event.target.id === 'doSomething') {
doSomething();
}
}

function doSomething() {}
|

過度に発火するイベントハンドラを絞ること
  • ハンドラが何度もコールされるなら、UIの応答性が悪くなり、CPUを猛烈に動かす。リサイズのようにイベントハンドラがリフローを引き起こすならこれは特に問題だ。
  • S60 5.0でバイス上では、例えば、ユーザーが一文字タイプしたりと、inputフィールドの中身が変わる度にinput要素のblurハンドラがコールされる。
  • 潜在的に高価な関数の余分なコールを抑制するために、最後のイベントが発火した後で一度だけ自分の関数を走らせたいかもしれない。一種の速度制限を達成するためのスロットル機構を実装しなければいけない。
  • If a handler is called many times, the responsiveness of the UI degrade and tops the CPU. This is especially an issue if the event handler triggers reflow as is the case with resize.
  • On S60 5.0 devices input element's blur handler is called each time the content of the input field changes, e.g. user types in a single letter.
  • You may want your function to run once after the last event has fired to prevent excessive calls to potentially expensive functions. You must implement throttling mechanism to achieve that kind of a rate limited.

遅い:

window.onresize = resizeHandler; // fires excessively during resize

速い:

function SomeObject() {
     var self = this;
     this.lastExecThrottle = 500; // limit to one call every "n" msec
     this.lastExec = new Date();
     this.timer = null;
     this.resizeHandler = function() {
         var d = new Date();
         if (d-self.lastExec < self.lastExecThrottle) {
             // This function has been called "too soon," before the allowed "rate" of twice per second
             // Set (or reset) timer so the throttled handler execution happens "n" msec from now instead
             if (self.timer) {
                 window.clearTimeout(self.timer);
             }
             self.timer = window.setTimeout(self.resizeHandler, self.lastExecThrottle);
             return false; // exit
        }
        self.lastExec = d; // update "last exec" time
        // At this point, actual handler code can be called (update positions, resize elements etc.)
        // self.callResizeHandlerFunctions();
    }
}
 
var someObject = new SomeObject();
window.onresize = someObject.resizeHandler;

ソース:

JavaScriptコールスタックをsetTimeで回避
  • イベントが発火した後走るハンドラを用意するのはあるごまかしが必要だ。イベントは、イベントハンドラコールスタックが開き、完全に閉じるまで結果を生み出さないから、従来のイベントハンドラではイベント後の環境で機能する方法はない。
  • 何かが遅延0のsetTimeoutを使ってコールされると、JavaScriptエンジンは(setTimeoutを呼び出したタスクで)忙しいことに気がつき、setTimeoutコードを現在のコールスタックが閉じた時にすぐに実行するためにキューに入れる。
  • このテクニックは、DOMの修正のような計算上重い演算を実行するに先立って、ローディングインディケータを表示したり非表示にしたりといったある機能を優先的に扱うのに使うことができる。
  • Setting up handlers that run after an event has fired needs some trickery. Because the event doesn’t take effect until the event-handling call stack opens and closes completely, there’s no way to work in a post-event environment via conventional event handlers.
  • When something is called using setTimeout with a delay of 0 the JavaScript engine notices it is busy (with a task which invoked setTimeout) and queues the setTimeout code for execution immediately after the current call stack closes.
  • This technique can be used to prioritize certain functionality such as showing and hiding loading indicator prior to executing a computationally heavy operation such as modifying the DOM.


典型的なシナリオでは、これはロードインディケータを可視化しない:

In a typical scenario this will not turn the loading indicator visible:

 showLoadingIndicator();
 doSomethingExpensive();


対処策は以下のようにsetTimeoutを使用することだ。(これがしていることは実際には、UIにロードインディケータを表示させるために実行を送らせているので、このアンチパターンを使う時には特に注意して下さい

Workaround is to use setTimeout as follows (please take extra care when using this anti-pattern as what it does is actually delays the execution in order to display the loading indicator in the UI):

function switchViews() {
    setTimeout(function() {
        showLoadingIndicator();
    }, 0);
 
    setTimeout(function() {
        doSomethingExpensive();
    }, 50);
}

ソース: