目次


Web Workers を使用して Web アプケーションのユーザビリティーを高める

Comments

はじめに

Ajax および Web 2.0 アプリケーションの出現により、エンド・ユーザーは Web アプリケーションが素早く応答することにすっかり慣れています。Web アプリケーションの応答時間をさらに短縮するには、取り除かなければならないボトルネックがあります。そのボトルネックの 1 つとして挙げられるのが、JavaScript の大量の計算とバックグラウンド I/O です。このボトルネックをメイン UI のレンダリング・プロセスから取り去らなければ、応答時間を短縮することはできません。そこで登場するのが、Web Workers です。

Web Workers 仕様は、スクリプトをユーザー・インターフェースのスクリプトとは別にバックグラウンドで実行する機能を提供します。つまり、長期間実行されるスクリプトを、クリックやその他のユーザー操作に応答するスクリプトによって中断されないようにできるということです。Web Workers では、ページの応答性を維持すると同時に、長時間のタスクを中断することなく実行できます。

Web Workers が登場する以前は、最新の Web アプリケーションの中核には JavaScript がありました。JavaScript と、JavaScript が使用する DOM は基本的にシングル・スレッドであり、任意の時点で実行できる JavaScript メソッドは 1 つだけに限られます。つまり、コンピューターが 4 基のコアを搭載しているとしても、長期の計算を実行しているコアは、そのうちの 1 基だけということになります。例えば、月に到達するまでの完全な軌道を計算している場合、ブラウザーは軌道を示す動画をレンダリングしている間、ユーザー・イベント (マウスのクリックやキーボードの入力) に応答することはできません。

Web Workers はシングル・スレッドという従来からの JavaScript の殻を破り、マルチスレッド・プログラミング・モデルを導入します。ワーカーは、スタンドアロンのスレッドです。ワーカーにより、多数のタスクを処理する Web アプリケーションはタスクを一度に 1 つずつ処理しなくても済むようになります。代わりに、アプリケーションは複数のタスクを複数のワーカーに割り当てることができます。

この記事では、Web Workers API について紹介し、具体的な例に沿って、Web Workers を使用して素早く Web ページをレンダリングする方法を説明します。

記事で使用するサンプル・アプリケーションのソース・コードは、「ダウンロード」セクションからダウンロードすることができます。

基本概念

Web Workers の基本コンポーネントは以下のとおりです。

ワーカー
バックグラウンドで動作して、メインのユーザー・インターフェースのスクリプトをブロックすることのない新しいスレッドのことです。ワーカー (これらのバックグラウンド・スクリプトの名称) は比較的重いため、多数のワーカーを同時に使用するようには意図されていません。

ワーカーは、並列計算、バックグラウンド I/O、クライアント・サイドのデータベース操作を含め、かなりの数のジョブをこなすことができます。ワーカーはメイン UI を中断したり、DOM を直接操作したりはしません。代わりにメイン・スレッドにメッセージを返し、メイン・スレッドにメイン UI を更新させます。

サブワーカー

ワーカーの中で作成されるワーカーです。サブワーカーは、親ページと同じ生成元の中でホストされる必要があります。サブワーカーの URI は、サブワーカーが所有するページのロケーションではなく、親ワーカーのロケーションに相対して解決されます。

共有ワーカー
複数の接続を介して複数のページで使用できるワーカーのことです。共有ワーカーは通常のワーカーとは多少動作が異なります。このフィーチャーをサポートしているブラウザーはわずかしかありません。

Web Workers API

このセクションでは、Web Workers API の基本について紹介します。

ワーカーを作成する

新規ワーカーを作成するには、ワーカー・スクリプト URI を唯一のパラメーターとして指定して、ワーカー・コンストラクターを呼び出すだけのことです。ワーカーが作成されると同時に、新しいスレッド (または、ブラウザーの実装によっては新規プロセス) が開始されます。

ワーカーが作業を終了した時点、またはエラーが発生した時点で、ワーク・インスタンスの onmessage および onerror プロパティーによって、ワーカーから通知を受けることができます。リスト 1 にサンプル・ワーカーを記載します。

リスト 1. サンプル・ワーカー myWorker.js
 // receive a message from the main JavaScript thread
onmessage = function(event) {
// do something in this worker
var info = event.data;
postMessage(info + “ from worker!”);
};

リスト 2 の JavaScript コードを実行すると、「Hello World from worker」と出力されることになります。

リスト 2. メイン JavaScript スレッドでのワーカー
// create a new worker
var myWorker = new Worker("myWorker.js");
// send a message to start the worker
var info = “Hello World”;
myWorker.postMessage(info);
// receive a message from the worker
myWorker.onmessage = function (event) {
// do something when receiving a message from worker
alert(event.data);
};

ワーカーを終了する

ワーカーはスレッド (本質的にはプロセス) であり、大量のリソースを使用する OS レベルのオブジェクトです。ワーカーに割り当てられたタスクが完了したとき、あるいは単純にワーカーをキルしたいときには、ワーカーの terminate メソッドを呼び出して実行中のワーカーを終了する必要があります。terminate メソッドを実行すると、ワーカーのスレッドまたはプロセスは、その操作を完了することも、クリーンアップすることもなく、即時にキルされます。リスト 3 に一例を記載します。

リスト 3. myWorker の終了
myWorker.terminate();

エラーを処理する

通常の JavaScript コードと同じように、実行中のワーカーにもランタイム・エラーが発生する可能性があります。エラーを処理するためには、ワーカーを対象とした onerror ハンドラーを設定しなければなりません。このハンドラーは、ワーカーでスクリプトを実行中にエラーが発生すると呼び出されます。イベントはバブル動作することはなく、イベントをキャンセルすることができます。デフォルトのアクションが行われないようにするためには、ワーカーはエラー・イベントのpreventDefault() メソッドを呼び出すことができます。

リスト 4. myWorker を対象としたエラー・ハンドラーの追加
myWorker.onerror = function(event){
console.log(event.message);
console.log(event.filename);
console.log(event.lineno);
}

エラー・イベントには以下の 3 つのフィールドがあります。デバッグには、これらのフィールドが役立ちます。

  • message: 人間が読めるエラー・メッセージ
  • filename: エラーが発生したスクリプト・ファイルの名前
  • lineno: エラーが発生したスクリプト・ファイルの行番号

スクリプトとライブラリーをインポートする

ワーカー・スレッドはグローバル関数、importScripts() にアクセスすることによって、スクリプトまたはライブラリーをスレッドのスコープにインポートすることができます。この関数は、インポートするリソースの URI を引数としていくつでも取ることが (あるいは引数に取らないことも) できます。

リスト 5. スクリプトのインポート
//import nothing
importScripts();
//import just graph.js
importScripts('graph.js');
//import two scripts
importScripts('graph.js', 'controller.js');

Web Workers の使用例

このセクションでは、Web Workers を使用する具体的な例を紹介します。例として取り上げるサンプル・アプリケーションは、複数の Dojo ベースの Website Displayer ウィジェットが含まれるページをレンダリングします。これらのウィジェットを使用する目的は、iFrame を使用した Web サイトを表示するためです。Web Workers を使用しないとしたら、Ajax リクエストでウィジェットの定義を取得し、たった 1 つの JavaScript スレッドでウィジェットをレンダリングすることになります。この場合、ウィジェットの定義に大量のデータが含まれているとしたら、レンダリングが完了するまでに相当な時間がかかることになります。

この例ではウィジェットの定義を取得するワーカーをいくつか作成します。ワーカーごとにウィジェットの定義を 1 つ取得するタスクを割り当て、各ワーカーは、メイン UI の JavaScript スレッドに対してウィジェットをレンダリングするよう指示します。ワーカーは並行して動作することができるので、この例は遥かに高速なソリューションとなります。

サンプル・アプリケーションで使用するのは Dojo 1.4 です。このサンプル・アプリケーションをご使用のブラウザーで実行するには、Dojoライブラリー (「参考文献」を参照) と記事で使用しているソース・コード (「ダウンロード」を参照) をダウンロードしてください。図 1 に、サンプル・アプリケーションの構造を示します。

図 1. Web Workers アプリケーション
「Web Workers」ディレクトリー配下のディレクトリー構造を示すスクリーン・ショット

図 1 に示されているディレクトリー構造を以下に説明します。

  • lib は、Dojo ライブラリーです。
  • /widgets/WebsiteDisplayer.js は、Dojo ベースの Website Displayer ウィジェットの実装です。
  • loadwidget/widgets/widgetDefinition[0....3] のそれぞれは、各 Website Displayer ウィジェットの定義です。
  • /loadwidget/Workers.js は、ワーカー実装です。
  • /loadwidget/XMLHttpRequest.js は、XMLHttpRequst を作成するためのメソッドが含まれる JavaScript ライブラリーです。
  • /loadwidget/LoadWidget.html は、Web Workers が有効にされた、デモのメイン・ページです。これが、メインの JavaScript スレッドとなります。
  • /loadwidget/LoadWidget-none-web-workers.html は、Web Workers を使わずに実装されたメイン・ページです。

Website Displayer ウィジェットを作成する

Website Displayer ウィジェットは Dojo TitlePane の dijit をベースとした極めて単純なウィジェットです。このウィジェットは、一定の形式のタイトル・ペインの UI をレンダリングします (図 2 を参照)。

図 2. Website Displayer ウィジェット
「Load Widget Use Web Workers」というタイトルが付いた Website Displayer ウィジェットのスクリーン・ショット
「Load Widget Use Web Workers」というタイトルが付いた Website Displayer ウィジェットのスクリーン・ショット

リスト 6 に、WebsiteDisplayer.js のコードを記載します。

リスト 6. WebsiteDisplayer.js の内容
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.require("dijit.TitlePane");

dojo.declare("loadWidget.WebsiteDisplayer", [dijit.TitlePane], {
    title: "",
    url: "",
    postCreate: function() {
	var ifrm = dojo.create("iframe", {
           src: this.url,
	    style: "width:100%;height:20%;"
	});
	dojo.place(ifrm, this.domNode.children[1], "first");
	this.inherited(arguments);
	var contentFrame = this.domNode.children[1].children[0];
	if (contentFrame.attachEvent) {
	    contentFrame.attachEvent("onload",
		function() {
		    dojo.publish("frameEvent/loaded");
		}
	    );
	} else {
	    contentFrame.onload = function() {
		dojo.publish("frameEvent/loaded");
	    };
	}
    }
});

ワーカーを作成する

worker.js を実装するには、XMLHttpRequest.js というグローバルな JavaScript ファイルをインポートします。このファイルにはグローバル・メソッド creatXMLHTTPRequest が含まれており、このメソッドが XMLHttpRequest オブジェクトを返します。

ワーカーはまず、XMLHttpRequest をサーバー・サイドに送信し、ウィジェット定義を取得してメイン JavaScript スレッドに渡します。リスト 7 とリスト 8 に例を示します。

リスト 7. Worker.js の内容
importScripts("XMLHttpRequest.js");

onmessage = function(event) {
  var xhr = creatXMLHTTPRequest();
  xhr.open('GET', 'widgets/widgetDefinition' + event.data + '.xml', true);
  xhr.send(null);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200 || xhr.status ==0) {
	 postMessage(xhr.responseText);
      } else {
	 throw xhr.status + xhr.responseText;
      }
    } 
  }
}
リスト 8. widgetDefinition0.xml
<div dojoType="loadWidget.WebsiteDisplayer" title="This is Test Widget 0"
   url="http://www.yahoo.com" ></div>

メイン Web ページを作成する

メイン Web ページで行う作業は、複数のワーカーを作成すること、メッセージをワーカーに送信してワーカーを起動すること、ワーカーからのメッセージを受け取り、そのメッセージを使ってメイン UI を操作することです。

リスト 9. メイン Web ページ
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>
            Load widgets with Web Workers
        </title>
        <style type="text/css">
            @import "../lib/dijit/themes/soria/soria.css";
	    @import "../lib/dojo/resources/dojo.css";
	    @import "../lib/dojox/layout/resources/GridContainer.css";
            @import "../lib/dojox/layout/resources/DndGridContainer.css"
        </style>
        <script type="text/javascript" src="../lib/dojo/dojo.js" 
           djConfig="parseOnLoad: true,isDebug:true">
        </script>
        <script>
            dojo.require("dojo.parser");
            dojo.require("dojo.io.script");
	     dojo.require("dojox.layout.GridContainer");
            dojo.require("dijit.layout.LayoutContainer");
            dojo.require("dijit.TitlePane");
            dojo.require("dojox.layout.DragPane");
            dojo.registerModulePath("loadWidget", "../../loadWidget");
            dojo.require("loadWidget.WebsiteDisplayer");
	</script>
	<script type="text/javascript" language="javascript">
            var workersCount = 4;
            var haveLoadedCount = 0;
            var widgetCount = 4;
            var startTime = new Date().getTime();
            var endTime = null;
            var executeTime = 0;
            try {
                for (var i = 0; i < workersCount; i++) {
                    var loadWorker = new Worker("Worker.js");
                    loadWorker.postMessage(i);
                    loadWorker.onmessage = processReturnWidgetDefinition;
					loadWorker.onerror = handleWorkerError;
                }
            } catch(ex) {
                console.log(ex);
            }
			
            function processReturnWidgetDefinition(event) {
                var txt = document.createElement("p");
                txt.innerHTML = event.data;
                var div = document.getElementById("loadingDiv");
                div.appendChild(txt);
                haveLoadedCount++;
                if (haveLoadedCount == widgetCount) {
                    dojo.parser.parse();
                }
            }
			
	     function handleWorkerError(event){
		  console.log(event.message);
	     }
			
            dojo.subscribe("frameEvent/loaded", dojo.hitch(null, handelFrameLoaded));

            function handelFrameLoaded() {
                if (haveLoadedCount == widgetCount) {
                    endTime = new Date().getTime();
                    executeTime = endTime - startTime;
                    dojo.byId("loading").innerHTML = "Loading cost time:" + executeTime;
                }
            }
        </script>
    </head>
	
    <body class="soria">
       <div dojoType="dijit.TitlePane" title="Load widgets with Web Workers" 
          style="border: 2px solid black; padding: 10px;"
        id="main">
            <div id="loadingDiv">
                <div id="loading">
                    Widgets are loading......
                </div>
            </div>
        </div>
    </body>
	
</html>

このメイン・ページを Web アプリケーションに組み込んで実行します。すると、図 3 のようなページが表示されます。

図 3. Web Workers によるウィジェットのロード
3 つの異なる Web サイトの一部を表示する 3 つのウィジェットのスクリーン・ショット
3 つの異なる Web サイトの一部を表示する 3 つのウィジェットのスクリーン・ショット

Web Workers を使用した場合と使用しない場合の違いを比較するには、LoadWidget.html と LoadWidget-none-web-workers.html を別々に実行して、その結果を調べてください。Web Workers を使用しないで実行するページは、Web Workers を使用して実行するページに比べ、短時間で終了しますが、これは、サンプル・コードで処理するデータはほんのわずかだからです。節約された分の時間は、ワーカーを起動するためにかかった時間に相当します。

Web Workers の使用に関する助言

上記のサンプル・アプリケーションには XMLHttpRequest と計算が伴いますが、これはそれほど大きなアプリケーションでも、複雑なアプリケーションでもありません。ワーカーに大量の計算を処理させるなど、これよりも複雑なタスクを割り当てるとしたら、ワーカーは非常に強力な機能になります。この素晴らしい技術をプロジェクトに採用する前に、以下の助言を検討してください。

ワーカー内で DOM にアクセスすることはできません

安全のため、ワーカーは HTML 文書を直接操作することはできません。複数のスレッドを同じ DOM 上で動作させると、スレッド・セキュリティーの問題が生じることになるからです。ワーカーを実装するメリットは、マルチスレッドの安全に関する問題を心配しなくても済むことです。

こうした状況では、ワーカーを開発する際にいくつかの制約が課せられることになります。例えば、JavaScript コードをデバッグする方法として非常によく使われている alert() をワーカー内で呼び出すことはできません。また、document.getElementById() は変数 (ストリング、配列、JSON オブジェクトなど) を受け取って返すことができるだけなので document.getElementById() の呼び出しを使用することもできません。

ワーカー内で使用できるオブジェクト

ワーカーは window オブジェクトにアクセスできないものの、navigator には直接アクセスすることができます。navigator オブジェクト内では、appNameappVersionplatform、および userAgent にアクセスすることができます。

location オブジェクトには読み取り専用でアクセスすることができます。したがって、location オブジェクトに含まれる hostname および port を取得することができます。

この記事の例で明らかなように、XMLHttpRequest はワーカー内でも有効です。この機能により、さまざまな興味深い拡張機能をワーカーに追加することができます。

さらに、以下のオブジェクトも使用することができます。

  • importScripts() メソッド (同一ドメイン内のスクリプト・ファイルにアクセスする場合)
  • ObjectArrayDateMathString などの JavaScript オブジェクト
  • setTimeout() および setInterval() メソッド

postMessage で送信されるデータ型

postMessage はメイン JavaScript スレッドがワーカーと対話するための主要なメソッドであることから、かなり頻繁に使用されます。ただし、現時点で postMessage で送信できるデータ型は、ネイティブJavaScript 型 (Array、Date、Math、String、JSON など) に限られます。複雑にカスタマイズされた JavaScript オブジェクトはあまり十分にはサポートされません。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=594588
ArticleTitle=Web Workers を使用して Web アプケーションのユーザビリティーを高める
publish-date=11092010