連載「Webサイト・アプリ高速化テクニック徹底解説」の第3回は、前回の「ユーザーの体感速度を高めるためのJavaScriptチューニング(前編)」の続きです。この後編では、「ユーザーの操作を阻害しない」方法についてJavaScriptのシングルスレッドやイベントループを交えて解説し、HTML5のWeb Workersについても紹介していきます。
ユーザーの体感速度を高めるためのJavaScriptチューニング(後編)
前回は、ユーザーの体感速度を向上させるための方法として、3つのうち「ページを素早く表示する」と「ユーザーに素早くインタラクションを返す」を解説しました。今回は、最後の「ユーザーの操作を阻害しない」について詳しく解説していきます。
ユーザーの操作を阻害しない
JavaScriptによる処理が重くなると、いつまでも画面が更新されなかったり、ユーザーの操作が止まってしまったりということがあります。止まっている時間が長すぎると、ブラウザから応答がないという警告がでることもあります。それほどひどくなくても、ページのスクロールが途中でつっかかったり、アニメーションがとぎれとぎれになったりといったことは、皆さん経験があるのではないでしょうか。こういった状態は、ブラウザのユーザーインターフェース周りの処理とJavaScriptが同じシングルスレッドで動作していることに原因があります。このようなブラウザにおけるシングルスレッドとイベントループという仕組みを先に簡単に解説しておきましょう。
シングルスレッドとイベントループ
通常、ほとんどのブラウザはシングルスレッドで動作しており、JavaScriptのコードや画面の描画、ユーザーの操作(ページのスクロールやマウス操作など)を同時に処理することはできません。そのため、JavaScriptの処理に時間が掛かってしまうと、その間は画面の描画も、ユーザーの操作も止まってしまいます。これらの処理は、イベントループと呼ばれる仕組みで動作しています。
イベントループは、イベントを監視する無限ループを持ち、イベント(またはそのコールバック)が発生すると、発生順にそのイベントを処理していきます。ここでいうイベントには、JavaScriptのイベントに加えて、ブラウザのレイアウトイベントや描画イベントなども含まれています。そのため、JavaScriptの実行に時間がかかると、その間に発生した描画イベントなどの他のイベントが実行キューに登録され、いつまでも処理されないということになります。根本的な対応方法は各処理を短くすることですが、このイベントループの仕組みうまく利用すれば、大きな処理を細かく分割することもできます。
setTimeout関数などによる擬似的な並列処理
イベントループの仕組みを考えると、JavaScriptの処理に時間がかかる場合は、その処理をいったん終了させて、別のイベントで続きを実行すれば処理を分割できることになります。この分割した処理の間に、ブラウザの描画イベントなどが発生すれば、正常に画面が更新されることになります。
例えば、JavaScriptで時間のかかる処理の前に、先に見た目を更新するという方法(前回の「ユーザーに素早くインタラクションを返す」)を挙げましたが、次のようなコードでは画面がすぐには更新されず、JavaScriptの実行が終わるまで反映されません。
:javascript:
// id属性に"output"を指定した要素にメッセージを表示するコード
var output = document.querySelector('#output');
output.textContent = 'メッセージ';
// ~なんらかの時間がかかる処理~
これを、メッセージがすぐに画面に反映されてから、時間がかかる処理をするように変更するには、次のように記述します。
:javascript:
// id属性に"output"を指定した要素にメッセージを表示するコード
var output = document.querySelector('#output');
output.textContent = 'メッセージ';
setTimeout(function(){
// ~なんらかの時間がかかる処理~
}, 0);
setTimeout関数を利用して、時間のかかる処理を実行しています。setTimeout関数は、一定時間後に指定したメソッドを実行する関数ですが、ここでは0ミリ秒後に指定しています。これは、実際にはすぐに実行されるわけではなく、イベントループの実行キューに登録されることになります。そのため、上で表示しているメッセージの描画イベントが先に実行され、その後にsetTimeout関数で指定した処理が実行されます。
このようにして、setTimeout関数を使って、明示的に大きな処理を分割し、擬似的な並列処理を実現することができます。あまり多用するとコードの可読性やメンテンナンス性などが落ちるので注意が必要です。また、setTimeout関数以外でもイベントやコールバックであれば良いので、XMLHttpRequestやその他のイベントハンドラでも同様のことができます。JavaScriptでは、もともとそういった記述が多いので、意図的ではなくても自然とそういったコードになっていることが多いと思います。しかしながら、こういった仕組みを知っておくことでうまく問題が解決できることもありますので、覚えておくと良いでしょう。
実は、このsetTimeout関数を使った特定の処理を実行キューの最後にスタックする方法は、setImmediate関数として標準化が議論されています(仕様はこちら)。現在は、IE9以降で実装されています。
バックグラウンドでJavaScriptを実行する「Web Workers」
HTML5には、setTimeout関数などによる擬似的な並列処理ではなく、本当の並列処理を行うためのWeb Workersという仕様があります。このWeb Workersでは、JavaScriptの処理をブラウザのメインスレッドとは別に、バックグラウンドで処理することができるようになります。そのため、画面の描画やユーザー操作を阻害せずに、時間のかかるJavaScriptを実行することができます。
早速、Web Workersの利用方法を見ていきましょう。Web Workersを利用する場合、バックグラウンドで処理するためのJavaScriptファイルを別に用意します。ここでは、表示するページで読み込むmain.js、バックグラウンドで動作するworker.jsの2つに分けています。それぞれのファイルの内容は、次の通りです。
main.js
:javascript:
// ワーカーの作成
var worker = new Worker('worker.js');
// メッセージ受信イベントの登録
worker.addEventListener('message', function(e){
console.log(e.data);
});
// メッセージ送信
var message = 'test';
worker.postMessage(message);
main.jsでは、バックグラウンドで動作するWorkerオブジェクト(以下、ワーカー)を引数にworker.jsを指定して作成します。そして、ワーカーとのデータの送受信には、お互いにメッセージをやり取りして行います。ワーカーのpostMessage()メソッドで加工するデータなどを送信し、messageイベントでワーカーから戻ってきたデータを受信します(データはコピー渡しとなります)。送受信できるデータは、基本的な型やオブジェクトなどが可能ですが、Errorオブジェクトや関数(Functionオブジェクト)、DOMノードなどはエラーとなります。
worker.js
:javascript:
self.addEventListener('message', function(e){
// メッセージを受信して、そのまま返す
self.postMessage(e.data);
});
worker.jsでは、main.jsと同じようにmessageイベントとpostMessage()メソッドを利用してメッセージをやり取りします。ここでは、messageイベントでデータを受け取り、受け取ったデータをpostMessage()メソッドでそのまま返しています。例えば何らかの処理をする際には、ここでデータを加工して送り返すといったように変更すると良いでしょう。ワーカー自身にアクセスするには、self変数を利用します。また、ワーカー内では、windowオブジェクトやページのDOMツリーにアクセスすることができませんので注意してください。
このようにして、main.jsでは基本的にユーザーインターフェースの作成やワーカーでの処理結果を反映するだけにとどめ、ワーカーで実際の処理をバックグラウンドで行うようにすると、ユーザーの操作を阻害しないような作り込みができます。また、Web Workersを利用することで、自然とビジネスロジックなどのコードを切り分けることできるので、MVCモデルのような構成を取りやすいこともメリットのひとつでしょう。是非、チャレンジしてみてください。
以降は、Web Workersの対応状況や制限事項などを載せておきますので、参考にしてください。
Web Workersの対応状況
デスクトップ向けであれば、IE9を除くすべてのモダンブラウザ(IEはバージョン10より対応)で利用することができます。モバイル向けでは、iOSは対応していますが、残念ながらAndroidの標準ブラウザが未対応です。そのため、Androidに対応するためにはfakeworker-jsなどのPolyfillを利用しましょう。
メッセージで交換できるデータの種類
メッセージで交換できるデータについては、Boolean、Number、Stringオブジェクトなどの基本的な型に加え、Date、RegExp、ImageData、File、Blob、FileList、Array、Objectオブジェクトなども可能です。逆に扱えないデータとしては、Error、FunctionなどのオブジェクトとDOMノード、RegExpオブジェクトのlastIndexプロパティなどです。また、setterやgetter、prototypeなどはコピーされません。詳しくは、Structured Cloneというアルゴリズムで定義されています。
ワーカー内で利用できる機能
ワーカー内では、DOMツリーやwindowオブジェクト、doucmentオブジェクト、parentオブジェクトにアクセスすることができません。ワーカー内で利用できる機能としては、navigatorオブジェクトやlocationオブジェクト(読み取り専用)、XMLHttpRequest、setTimeoutとsetInterval関数、Application Cacheなどです。また、さらに他のワーカーを作成することもできます。ワーカーで利用できる独自の機能には、外部スクリプトをインポートすることができるimportScripts関数などがあります。
おわりに
前編と後編にわたって、ユーザーの体感速度を向上させるJavaScriptのチューニング方法について詳しく解説しました。次回からは、引き続きJavaScriptの高速化をテーマに、DOM操作の最適化について解説していく予定です。お楽しみに!