はじめに
人間は生活の中で、多くのアクションを起こし、多くのリアクションを受け取ります。それがどのようなカラクリで動いているのか知らなくても、期待するリアクションが起きれば、人間は満足するのです。
水道の蛇口は「ひねる」というアクションに対して「水がでる」というリアクションを起こします。そして、壁のスイッチは「押す」というアクションに対して「灯が点く」というリアクションを起こします。そこに人間は、何の不満も抱いたりはしないでしょう。
とはいえ、コンピュータはリアクションが起きるまでに、必ず遅延が発生してしまいます。コンピュータは、入力というアクションから出力というリアクションまでの間に、何かしらの処理を行わなくてはいけないからです。この処理が長引くほど、リアクションは遅れ、人間は違和感や不快感を持ちます。
Webのパフォーマンス基準であるRAILでは、こうしたリアクション、すなわち「Response(応答) 」について、100ミリ秒未満であることを推奨しています。また、処理が完了するタイミング、すなわち「Idle(非実行) 」となるまでの時間が、100ミリ秒未満になることを推奨しています。
何も難しいことはありません。この2つのキーワードは、本質的には同じことを意味しています。要はブラウザの仕組み上どうしても発生しがちな「ブロッキング」が、100ミリ秒未満で終わるようにすべきだ、と言っているだけのことです。
ブロッキングとは、あるタスクが動いているがために、別のタスクが動けなくなる状態のことです。ブラウザには、このような状態に陥るポイントが所々に存在し、最適化することが求められています。
シングルスレッドであるがゆに生じたブロッキング問題
まずはJavaScriptのシングルスレッド問題と、その対処方法を紹介します。そのためにまずはブラウザの仕組みを、ざっくりと把握してみましょう。
ブラウザは、通信処理や画面上への描画といった低レイヤーなもの以外は全て、1つのスレッドで動作します。HTMLやCSSのパース、レイアウト処理、JavaScriptの実行といった、あらゆる処理は同じ時間に1つだけ実行されるのです。ブラウザを取り巻く様々な技術は、この前提があるからこそ成り立っています。
図1 ブラウザの仕組み
世の中にはブラウザのレンダリングエンジン以外にも、様々なエンジンが存在します。ゲームエンジンもそうですし、OSも広義ではエンジンと捉えることができます。そしてその全てに共通して言えることですが、リアルタイムにデバイスからのフィードバックを必要としないような、それこそ、ブラウザのようなUIのイベントやレンダリングを中核とするようなエンジンは、シングルスレッドで動くにようにするのが一番シンプルになります。
それは、ブラウザが持つ機能をシンプルに、そして直感的なものにするため、開発者である我々も幸せになれます。ただ、当然ですがそこには弊害もあります。
たとえば、JavaScriptで非常に重い処理を行っているとします。その処理が完了するのには時間がかかります。同じ時間の中で実行できるタスクは1つであり、完了するまで他のタスクは処理されないため、他のタスクには長い待ちが生じます。ユーザのイベントを受け付けて別の処理を行ったり、レイアウトを変更して再描画するといった処理は行えません。
また、JavaScriptで処理を続けている間、ユーザは何のリアクションも受け取れません。古いWindowsだと、ウィンドウ内が真っ白になることもあります。そうでなくても、反応までに時間がかかると、ユーザはアクションが成功しているのかがわからず不安となり、再びボタンを押してみるなどのリトライを試みます。
ではこの重いJavaScriptの処理を、どのように扱えば良いのでしょうか。100ミリ秒未満に抑えるには、どのような対策が必要でしょうか。
作戦1.処理を分割する
マルチタスクがシンプルな仕組みでできているような(主に組込系の)OSには、yieldと呼ばれるAPIが実装されていることがあります。他のタスクも並列して処理できるよう、タスクを途中で中断し、別のタスクを処理できる仕組みです。タスクの切り替えを、プログラム上で意図したタイミングで発生させることができたりします。
この状況は、シングルスレッドでしか動かないブラウザも同じと言えるでしょう。JavaScriptもまた、時間のかかるタスクの途中には処理を中断させ、他のタスクも処理を行わせなくてはいけません。
このあたり、そのまま同じ名称のまま、JavaScriptの次バージョンであるECMAScript 6に「generators」という機能として実装されています。yieldを呼び出すことで処理を止め、次回からは中断された場所から処理を再開できます。
function * Hoge () {
/* 処理A */
yield ; // 中断
/* 処理B */
}
var hoge = Hoge ();
hoge . next (); // 処理Aを実行
/* 他の処理 */
hoge . next (); // 処理Bを実行
ただ、ブラウザの場合、タスクを複数の処理に単純に分割するだけではなく、UI側に制御を渡してイベントを拾ったり、あるいはレイアウトを変えて描画させたりと、JavaScriptの外の世界が身動きできるようにしてあげる必要があります。generatorsでは、根本的なユースケースが異なってきます。
そこで、いかにしてJavaScriptが処理待ちをしていない状態、すなわちIdleという状態を作るのかが重要になってきます。古いAPIの中でできる最もシンプルな方法は、setTimeoutを使うことです。0ミリ秒後にコールバックを呼び出せという命令は、内部的にはわずかなIdle状態が生じ、他のタスクが動く隙を作ります。筆者自身も昔、現場でこの方法を活用した事例を目にしたことがあります。
/* 処理一旦停止 */
setTimeout ( function (){
/* 処理再開 */
}, 0 );
ただ、この方法はバッドノウハウと言わざるえません。仕組み上、たまたま使えたと言ってもいいでしょう。setTimeoutの精度のゆるさが、このような使われ方を許したのです。
とは言え、このようなユースケースで使えるようなAPIを、正式な仕様として扱えるようにしようという動きはあったりします。それがWeb標準「Efficient Script Yielding 」です。この仕様には、setImmediateというAPIが含まれており、再描画やイベントの受付けを行うような余裕を与えてくれます。
/* 処理一旦停止 */
setImmediate ( function (){
/* 処理再開 */
});
これで、一応は筋が通るようになりました。しかし現在、この仕様の策定は完全に停滞しており、IEぐらいしか実装ブラウザがありません。いつ消えてもおかしくないようにも見えます。Chromeにも実装して欲しいという声が挙がっていますが、今もなお実装されていません。
では、何が正解なのでしょうか? 実は、setImmediateで実現したいと考えていることは、Web標準「Timing control for script-based animations 」でカバーできるという意見があります。画面の描画が終わったタイミングで、コールバックを呼び出すというAPIです。現段階では、これが一番有力に見えます。
/* 処理一旦停止 */
requestAnimationFrame ( function (){
/* 処理再開 */
});
ただ、やはりですが「これじゃない!」という意見が出ています。我々の悩みはIdleという状態と向き合うことなのだから、タスクスケジューリングというレベルでスレッドを扱えるようにしてはどうか? Idle状態になったタイミングを狙ってコールバックを呼び出すという「requestIdleFrame」を実装してみてはどうか? という、意見も出てきていたりします。しかし現時点では、実現には至っていません。
作戦2.別のスレッドで処理する
処理の量が多いというのであれば、思い切って別のスレッドで実行してしまうというのも手です。先ほど説明したECMAScript 6のgeneratorsも確かに有用ですが、CPUのコアが2つ以上あるのが当たり前な時代、これを活用しないという手はありません。Web標準の「Web Workers 」という仕様が、まさにこの用途のために作られた機能です。
先ほども述べましたが、UIイベントや描画周りは、基本的にはシングルスレッドで動かさなくてはいけません。これは技術的な制約が原因です。複数スレッドからレンダリングを行えるような環境もありますが、ブラウザはあらゆる環境で動くことが求められている以上、それを前提にはできません。
Web Workersでは、ワーカーと呼ばれる新しいスレッドを生成し、処理の一部を委譲できます。そしてそのワーカーは、先ほどの技術的制約により機能が制限されています。UI周りを操作するには、メインスレッド側とメッセージを通じたコミュニケーションが必要です。
図2 Web Workersの仕組み
ワーカーを作成し、そこへメッセージが送られると処理が開始されるというサンプルコードも載せておきます。非常にシンプルですね。
呼び出し元
var worker = new Worker ( "worker.js" );
worker . postMessage ([]);
ワーカー側(worker.js)
onmessage = function ( event ) {
/* 処理 */
}
作戦3.処理効率を上げる
最近、WebAssemblyと呼ばれる技術が話題を集めています。JavaScriptが担っていた領域をバイナリで実現できるため、高速なコンパイルた期待できるでしょう。ただ、CPUの効率化という意味では、Intelが中心として検討を進めているECMAScript 7の「SIMD.js 」が期待できるでしょう。SIMDとは、1つの命令だけで複数のデータを処理するというCPUの機能です。大量のデータに対して単純な計算処理が必要というのであれば、それを効率的に扱うことができます。
図3 Introducing SIMD.js ☆ Mozilla Hacks - the Web developer blog
Microsoftも4月頃から開発中のステータスへ変わっています。 こちらは今後に期待です。
WebAssemblyやSIMDはCPUを効率化させますが、並列計算といえばGPU。Web標準としては「WebCL 」が挙げられますが、今のところ主要ブラウザのサポートはありません。FirefoxやWebkitのアドオンという形で提供されているようです。
JavaScriptのパフォーマンス計測
さて、ここからは計測の話をしましょう。JavaScriptのパフォーマンスについて、問題を把握する方法は2つあります。開発ツールを使うという方法と、JavaScript APIを使うという方法です。
デバッグや問題解析時に、ある特定のJavaScriptコードのパフォーマンスだけを計測したいというのであれば、console.timingが役立ちます。ブラウザ独自実装な機能なうえ、printfデバッグの香りを感じさせますが、ちょっとした検証には充分役立つでしょう。
console . time ( "name" ); // 計測開始
/* 計測対象の処理 */
console . timeEnd ( "name" ); // 計測終了 & 結果の出力
どこが遅いのかを特定する際には、開発ツールのプロファイラが有用でしょう。最近の動きとしては「Firefox Developer Edition 」の40から、Call Treeという機能が実装されました。これは呼び出したJavaScriptメソッドと時間をツリー表示させることで、どこにどれだけの時間がかかっているのかを把握できます。
図4 Call Tree機能使用例
Microsoftのブラウザにも類似する機能が実装されており、今後はより一層、ブラウザの開発ツールによる分析が捗りそうです。
実運用の中で状況を把握したい場合は、JavaScript上でクロスブラウザで動作する、標準なAPIを使って計測したいところです。Web標準に「User Timing 」という仕様があります。「 Can I use 」を覗いてみても分かる通り、そこそこに速い時期に実装され、Safari以外のほとんどブラウザで実装されています。
図5 Can I useのUser Timing API
使い方も非常にシンプルです。計測は以下の通りです。
performance . mark ( "startTask" );
/* 計測対象の処理 */
performance . mark ( "endTask" );
結果は以下のように取得できます。
var entries = performance . getEntriesByType ( "mark" );
for ( var i = 0 ; i < entries . length ; i ++ ) {
entry = entries [ i ];
if ( entry . name === "startTask" ) {
console . log ( "開始:" + entry . startTime + "ミリ秒" );
} else if ( entry . name === "endTask" ) {
console . log ( "終了:" + entry . startTime + "ミリ秒" );
}
}
最後に
ブロッキングの問題は、ここで取り上げたものが全てではありません。Webページの読み込み時に、スレッドとは異なる要因で発生します。詳しくは次回のテーマ「Load」にて、まとめてお話させていただきます。
それでは次回また、お会いしましょう。