JSDeferredで,面倒な非同期処理とサヨナラ

第4回 JSDeferredを使いJSアプリのパフォーマンスを最適化する

この記事を読むのに必要な時間:およそ 2.5 分

前回まででJSDeferredの基本的な使いかたを説明しました。

今回は「非同期処理を簡単にする」から一歩進んで,既存アプリケーションの「非同期処理でパフォーマンス改善する」ことや,その他応用できる部分について説明します。

UIスレッド

JavaScriptが担当するアプリケーションユーザインターフェイスの「はやさ」とはなんでしょうか? 単純に,最速で処理が終わることというのも一つですが,それを含めて最も重要なのは,ユーザにストレスを与えないことでしょう。多少実行速度が遅くても大抵待っていられますが,インターフェイスがなんの反応もしないと大きなストレスになります。

JavaScriptを実行するブラウザには,UIスレッドを呼ばれる,システム入出力を処理する流れがあります。JavaScriptは,その性質上どうしてもUIスレッド上で動かす必要があります(JavaScriptを実行中にページの内容が割り込みで変化されたりすると整合性がとれなくて困りますよね)⁠

JavaScriptは,UIスレッド上で動くスクリプトであるため,JavaScript実行中,ブラウザは一切のUIに関する処理を行えなくなります。どんなブラウザであってもjavascript:for(;;);で数秒固まってしまうのはこのためです。ブラウザごとにUIスレッドの粒度は違いますが(ブラウザ全体で1つのUIスレッドであるか,ページごとであるか)⁠少なくともページごとには存在します。

UIスレッドの実行をできるだけ止めない

UIスレッドを長時間(数百ミリ秒)止めると,ユーザに大きなストレスを与えます。あるいはあまりに長時間(数秒)になるとブラウザによってダイアログが表示されたりして,強制的に遮断されることになります。

リスト1 

var n = 1000000;

for (var i = 0; i < n; i++) {
  // do something
}

では,そうしないためにはどうすればよいかというと,単純にsetTimeout()関数を使って,一旦JavaScriptの実行を止めてしまいます。それにより,UIスレッドはJavaScript実行以外の処理を間に挟むことができます。

setTimeout()関数は指定時間後に指定した関数を実行する関数ですが,時間に0を指定すれば,UIスレッドが空き次第,JavaScriptを実行するような挙動にすることができます(実際にはブラウザはsetTimeoutの監視をそう頻繁には行わないため,どのブラウザであっても最低10msec程度は遅延します)⁠

リスト2 

setTimeout(function () {
  // do something
}, 0);

例えば,forループをsetTimeout()関数を使い,できるだけブロックしないようにリスト1を記述しなおすと,以下のようになります。

リスト3

var n = 1000000;

var i = 0;
setTimeout(function () {
  if (i < n) {
    // do something
    i++;
    setTimeout(arguments.callee, 0);
  }
}, 0);

単純にforのループの切れ目でsetTimeoutするようにしただけですが,大変複雑になったように見えます。また,このループの処理の後にさらに処理を続けていきたいとき,面倒くさいですね。

全体が非同期の流れになっているので,ここでもJSDeferredが活躍できます。

JSDeferredを使うようにする

では,JSDeferredで処理を分割してUIスレッドの処理をブロックしないようにしてみましょう。まずはDeferred.next()関数がsetTimeout()相当のことをする(内部でsetTimeoutを使っている)ことを思いだして,上記setTimeoutの例(リスト3)を記述しなおしてみます。

リスト4 

var n = 1000000;

var i = 0;
next(function () {
  if (i < n) {
    // do something
    return next(arguments.callee);
  }
}).
next(function () {
  alert('done');
});

とりあえずこれで,ループが終ったあとの処理は簡単に追加できるようになりました。しかしループ自体はあまり綺麗ではありません。

とはいっても,JSDeferredにはloop()関数があり,これを使えば簡単にループを分割して処理できるようになります。

リスト5 

var n = 1000000;

loop(n, function (i) {
  // do something
}).
next(function () {
  alert('done');
});

とても簡単になりました! UIもブロックせず簡潔です。ただし,総合的な実行時間は,UIスレッドの実行を挟んだり,setTimeoutの処理をするため低下します。

著者プロフィール

cho45(さとう)

はてなエンジニア。サブテカ。バックエンドからインターフェイスまで,Perl,Ruby,JavaScript,ActionScriptなどを使いつつ Scala,Ioなどを触る,言語・コード表現ヲタク。

URLhttp://www.lowreal.net/
技術ネタhttp://subtech.g.hatena.ne.jp/cho45/

コメント

コメントの記入