ウェブ ワーカーの基本

HTML5 Rocks

JavaScript の並行処理という問題

面白いアプリケーションがあっても、(サーバーに置かれた容量の大きい実装などから)クライアントサイドの JavaScript へ移植しようとすると、数々のボトルネックを解消する必要があります。たとえば、ブラウザの互換性、静的型付け、アクセシビリティ、パフォーマンスなどの問題があります。幸い、ブラウザの JavaScript エンジンの処理速度は急速に改善されているので、これらの問題は過去のものとなりつつあります。

JavaScript の障害として残っているのは、実のところ言語そのものです。JavaScript は単一スレッド環境であり、複数のスクリプトを同時に実行することはできません。たとえば、UI イベントの処理、大量の API データのクエリと処理、DOM 操作をサイトで行わなければならない状況を思い浮かべてみてください。よくある状況ですが、残念なことにブラウザの JavaScript 実行時の制約により、すべてを同時に実行することはできません。スクリプトの実行は単一スレッド内で行われます。

デベロッパーたちは、setTimeout()setInterval()XMLHttpRequest、イベント ハンドラなどの手法を使って疑似の「並行処理」を実現しています。確かにこれらの機能はすべて非同期で実行されますが、「ノンブロッキング」は並行処理でなければならないことはありません。非同期イベントは、現在実行中のスクリプトの結果が生成された後で処理されます。HTML5 では、このような裏技よりも良い方法を使用できます。

ウェブ ワーカーの概要: JavaScript にスレッド機能を追加する

ウェブ ワーカーの仕様(英語)では、ウェブ アプリケーションでバックグラウンド スクリプトを生成するための API が定義されています。ウェブ ワーカーを使用すると、UI や他のスクリプトによるユーザー インタラクションの処理をブロックすることなく、長時間実行のスクリプトによる計算集約型のタスクの処理などができるようになります。これまでいつも表示されていた「応答のないスクリプト」というエラー メッセージを目にすることもなくなります:

「応答のないスクリプト」エラー メッセージ
よくある「応答のないスクリプト」エラー メッセージ

ワーカーでは、スレッド形式のメッセージ パッシングによって並列性が実現されます。常に最新の状態を反映した UI、パフォーマンスと応答性の高い UI をユーザーに提供するには、ワーカーを使用するのが最適です。

ウェブ ワーカーのタイプ

ウェブ ワーカーの仕様には、専用ワーカー共有ワーカー(リンク先はいずれも英語)という 2 種類のウェブ ワーカーについての記述がありますが、この記事では専用ワーカーのみを取り上げ、このワーカーを「ウェブ ワーカー」または「ワーカー」と呼ぶことにします。

ワーカーを使用する

ウェブ ワーカーは独立したスレッドで動作するので、ウェブ ワーカーによって実行されるコードは個別のファイルに格納する必要があります。しかしその前に、最初の作業としてメイン ページで新しい Worker オブジェクトを作成する必要があります。このコンストラクタはワーカー スクリプトの名前をとります。

var worker = new Worker('task.js');

指定されたファイルが存在する場合、ブラウザは新しいワーカー スレッドを生成します。このスレッドは非同期でダウンロードされます。ファイルのダウンロードが完了してファイルが実行されるまで、ワーカーは開始されません。ワーカーへのパスで 404 が返された場合、ワーカーはエラーを返すことなく失敗します。

ワーカーを作成したら、postMessage() メソッドを呼び出してワーカーを開始します:

worker.postMessage(); // Start the worker.

メッセージ パッシングによりワーカーと通信する

ワーカーと親ファイルの通信は、イベント モデルと postMessage() メソッドを使って行われます。ブラウザの種類とバージョンによって、postMessage() は文字列または JSON オブジェクトを単一の引数として受け入れることができます。近年のブラウザの最新バージョンでは、JSON オブジェクトの受け渡しがサポートされています。

下記は、文字列を使って「Hello World」を doWork.js のワーカーに渡す例です。このワーカーは単純に、渡されたメッセージを返します。

メイン スクリプト:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
  console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js(ワーカー):

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);

postMessage() がメイン ページから呼び出されると、ワーカーはそのメッセージを message イベントの onmessage ハンドラを定義して処理します。メッセージのペイロード(この場合は「Hello World」)には Event.data でアクセスできます。この例はそれほど面白いものではありませんが、postMessage() はデータをメイン スレッドに送り戻す手段としても使用できる便利なメソッドであることがわかるかと思います。

メイン ページとワーカーの間で受け渡されるメッセージはコピーされ、共有はされません。たとえば、次の例にある JSON メッセージの「msg」プロパティにはどちらの側でもアクセスできます。オブジェクトは、分離された専用の領域で実行されていてもワーカーに直接渡されているように見えます。実際の動作は、オブジェクトはワーカーに渡されるときにシリアル化され、その後メイン ページ側に渡ったときにシリアル化が解除されます。ページとワーカーは同じインスタンスを共有しないので、結果として、受け渡しごとに複製が作成されることになります。ほとんどのブラウザは、いずれかの側で値を自動的に JSON にエンコード/デコードすることにより、この機能を実装しています。

下記はもう少し複雑な、JSON オブジェクトを使ってメッセージを受け渡す例です。

メイン スクリプト:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
  function sayHI() {
    worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
  }

  function stop() {
    // Calling worker.terminate() from this script would also stop the worker.
    worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
  }

  function unknownCmd() {
    worker.postMessage({'cmd': 'foobard', 'msg': '???'});
  }

  var worker = new Worker('doWork2.js');

  worker.addEventListener('message', function(e) {
    document.getElementById('result').textContent = e.data;
  }, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)');
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

: ワーカーを停止するには、メイン ページから worker.terminate() を呼び出す方法と、ワーカー自体の内部で self.close() を呼び出す方法の 2 通りがあります。

: このワーカーを実行してみてください。

ワーカーを取り巻く環境

ワーカーのスコープ

ワーカーのコンテキストでは、selfthis はいずれもワーカーのグローバル スコープを参照します。したがって、上記の例は次のように記述することもできます:

addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
  ...
}, false);

または、onmessage イベント ハンドラを直接設定することもできます(ただし JavaScript を使いこなすデベロッパーたちは常に addEventListener の方をすすめています)。

onmessage = function(e) {
  var data = e.data;
  ...
};

ワーカーで使用できる機能

ウェブ ワーカーの動作はマルチスレッドになるので、アクセスできるのは次の JavaScript 機能のサブセットのみとなります:

ワーカーで次の機能にはアクセスできません:

  • DOM(非スレッドセーフ)
  • window オブジェクト
  • document オブジェクト
  • parent オブジェクト

外部スクリプトを読み込む

importScripts() 関数を使って外部スクリプト ファイルやライブラリをワーカーに読み込むことができます。このメソッドは、インポートするリソースのファイル名を表すゼロ以上の文字列をとります。

次は、script1.jsscript2.js をワーカーに読み込む例です:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

上記は 1 つのインポート ステートメントとして次のように記述することもできます:

importScripts('script1.js', 'script2.js');

サブワーカー

ワーカーは子ワーカーを生成できるので、実行時に大きなタスクを細かく分割するのに便利です。ただし、サブワーカーを使用する際は気を付けることがいくつかあります:

  • サブワーカーは親ページと同じ生成元内でホストされる必要があります。
  • サブワーカー内の URI は(メイン ページではなく)親ワーカーからの相対位置として解決されます。

ほとんどのブラウザでは、ワーカーごとに個別のプロセスが生成されます。ワーカー ファームを生成しようとするときには、ユーザーのシステム リソースを多く消費してしまわないように注意してください。この理由の 1 つは、メイン ページとワーカーの間で受け渡しされるメッセージは共有されずにコピーされるためです。メッセージ パッシングを使ってワーカーと通信するをご覧ください。

サブワーカーを生成する方法の例については、仕様内のこちらの例をご覧ください。

インライン ワーカー

ワーカー スクリプトをその場で作成する必要があるときや、別途ワーカー ファイルを作成せずに自己完結型のページを作成したいときはどうすればよいでしょうか。新しい BlobBuilder(リンク先は英語)インターフェースを使用すると、メイン ロジックと同じ HTML ファイルにワーカーを組み込んでインライン ワーカーにできます。これには、次のように BlobBuilder を作成しワーカー コードを文字列として追加します:

// Prefixed in Webkit, Chrome 12, and FF6: window.WebKitBlobBuilder, window.MozBlobBuilder
var bb = new BlobBuilder();
bb.append("onmessage = function(e) { postMessage('msg from worker'); }");

// Obtain a blob URL reference to our worker 'file'.
// Note: window.webkitURL.createObjectURL() in Chrome 10+.
var blobURL = window.URL.createObjectURL(bb.getBlob());

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  // e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

Blob URL

ポイントとなるのは window.URL.createObjectURL()(リンク先は英語)の呼び出しです。このメソッドでは単純な URL 文字列が作成されますが、これを DOM の File オブジェクトまたは Blob オブジェクトに格納されているデータの参照に使用できます。たとえば次のようにできます:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

Blob URL は一意な URL で、アプリケーションの実行期間中(document がアンロードされるまで)有効です。Blob URL を多く作成する場合は、不要になった参照から解放していくことをおすすめします。Blob URL を明示的に解放するには、Blob URL を window.URL.revokeObjectURL()(リンク先は英語)に渡します:

window.URL.revokeObjectURL(blobURL); // window.webkitURL.createObjectURL() in Chrome 10+.

Chrome では、chrome://blob-internals/ とアドレスバーに入力すると、作成したすべての Blob URL を確認できるので便利です。

全体の例

もう一歩進んで、ワーカーの JS コードをページに組み込む(インラインにする)スマートな方法を紹介しましょう。この手法では、<script> タグを使用してワーカーを定義します:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>

  <div id="log"></div>

  <script id="worker1" type="javascript/worker">
    // This script won't be parsed by JS engines because its type is javascript/worker.
    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  </script>

  <script>
    function log(msg) {
      // Use a fragment: browser will only render/reflow once.
      var fragment = document.createDocumentFragment();
      fragment.appendChild(document.createTextNode(msg));
      fragment.appendChild(document.createElement('br'));

      document.querySelector("#log").appendChild(fragment);
    }

    var bb = new BlobBuilder();
    bb.append(document.querySelector('#worker1').textContent);

    // Note: window.webkitURL.createObjectURL() in Chrome 10+.
    var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
    worker.onmessage = function(e) {
      log("Received: " + e.data);
    }
    worker.postMessage(); // Start the worker.
  </script>
</body>
</html>

どちらかというと、この新しい方法の方がすっきりして読みやすいと思います。ここではスクリプト タグの定義として id="worker1"type='javascript/worker' を指定しています(したがってブラウザは JS を解析しません)。このコードは document.querySelector('#worker1').textContent を使って文字列として抽出され、BlobBuilder.append() に渡されます。

外部スクリプトを読み込む

以上の手法を使ってワーカー コードをインラインにした場合、importScripts() が動作するには絶対 URI の指定が必要になります。相対 URI を渡そうとすると、ブラウザはセキュリティ エラーを返します。その理由は、(この時点で Blob URL から作成されている)ワーカーは blob: プレフィックスを使って解決されるのに対し、アプリは別のスキーム(おそらく http://)から実行されることになるためです。つまり、クロスドメイン制約によりエラーが発生します。

インライン ワーカーで importScripts() を利用する 1 つの方法としては、メイン スクリプト実行元の現在の URL を指定します。具体的には、URL をインライン ワーカーに渡し、手動で絶対 URL を作成します。これで、外部スクリプトが同一生成元からインポートされるようにできます。メイン アプリの実行元が http://example.com/index.html であるとすると、次のようになります:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
  var data = e.data;

  if (data.url) {
    var url = data.url.href;
    var index = url.indexOf('index.html');
    if (index != -1) {
      url = url.substring(0, index);
    }
    importScripts(url + 'engine.js');
  }
  ...
};
</script>
<script>
  var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
  worker.postMessage({url: document.location});
</script>

エラーを処理する

どの JavaScript ロジックでも言えることですが、ウェブ ワーカーで発生するエラーの処理を考える必要があります。実行中のワーカーでエラーが発生した場合は、ErrorEvent が発生します。インターフェースの 3 つのプロパティを確認すると、原因を突き止める参考になります。1 つ目は filename で、これはエラーの原因となったワーカー スクリプトの名前を表します。2 つ目は lineno で、これはエラーが発生した行番号を表します。3 つ目は message で、これはエラーのわかりやすい説明です。エラーのプロパティを出力する onerror イベント ハンドラのセットアップ例を次に示します:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
  function onError(e) {
    document.getElementById('error').textContent = [
      'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
  }

  function onMsg(e) {
    document.getElementById('result').textContent = e.data;
  }

  var worker = new Worker('workerWithError.js');
  worker.addEventListener('message', onMsg, false);
  worker.addEventListener('error', onError, false);
  worker.postMessage(); // Start worker without a message.
</script>

: workerWithError.js が 1/x を実行しようとする例を示します(x は未定義の値です)。

workerWithError.js:

self.addEventListener('message', function(e) {
  postMessage(1/x); // Intentional error.
};

セキュリティについて

ローカル アクセスでの制約

Google Chrome のセキュリティ制約により、このブラウザの最新バージョンではワーカーをローカルで実行すること(file:// など)はできなくなります。実行しようとすると失敗し、その際エラーは返されません。file:// スキームからアプリを実行するには、--allow-file-access-from-files フラグ セットを指定して Chrome を実行します。: このフラグ セットを使ってメインのブラウザを実行することはおすすめしません。これはテスト目的でのみ使用し、通常のブラウザ利用では使用しないでください。

他のブラウザにこれと同じ制約はありません。

同一生成元に関する注意事項

ワーカー スクリプトは、呼び出しページと同じスキームを指定した外部ファイルである必要があります。つまり、data: URL や javascript: URL からスクリプトを読み込むことはできません。また、https: ページは http: URL で始まるワーカー スクリプトを開始できません。

使用例

それでは、どのようなアプリでウェブ ワーカーを活用できるでしょうか。残念ながらウェブ ワーカーは比較的新しい技術で、現在出回っているサンプルやチュートリアルの大半は素数計算が中心です。面白みには欠けますが、ウェブ ワーカーの概念を理解する上では参考になります。下記にその他のアイデアをいくつか挙げます:

  • (後でデータを使用する目的での)データのプリフェッチ/キャッシュ
  • コード構文のハイライト表示、その他のリアルタイムでのテキスト書式設定
  • スペル チェック
  • 動画データや音声データの解析
  • バックグラウンド I/O やウェブサービスのポーリング
  • 大きな配列や膨大な JSON 応答の処理
  • <canvas> での画像フィルタリング
  • ローカル ウェブ データベースの行の大量更新

デモ

参考資料

Comments

0