ウェブワーカーの基本

問題: JavaScript の同時実行

多くのボトルネックがあるため、興味深いアプリケーションを(たとえば、サーバー負荷の高い実装から)クライアントサイドの JavaScript に移植できません。たとえば、ブラウザの互換性、静的型付け、ユーザー補助、パフォーマンスなどです。幸い、ブラウザ ベンダーは JavaScript エンジンの速度を急速に向上させているため、後者は急速に過去のものになりつつあります。

JavaScript の障害として残っているものの一つは、実際には言語そのものです。JavaScript はシングルスレッド環境です。つまり、複数のスクリプトを同時に実行することはできません。たとえば、UI イベントの処理、大量の API データのクエリと処理、DOM の操作が必要なサイトについて考えてみましょう。ごく一般的なことです。残念ながら、ブラウザの JavaScript ランタイムの制限により、これらすべてを同時に行うことはできません。スクリプトの実行は単一のスレッド内で行われます。

デベロッパーは、setTimeout()setInterval()XMLHttpRequest、イベント ハンドラなどの手法を使用して「同時実行」を模倣します。はい。これらの機能はすべて非同期的に実行されますが、非ブロック化は必ずしも同時実行を意味するわけではありません。非同期イベントは、現在実行中のスクリプトが生成した後、処理されます。幸いなことに、HTML5 はもっとうまく機能しています。

ウェブ ワーカーの紹介: JavaScript へのスレッド化

ウェブ ワーカーの仕様では、ウェブ アプリケーションでバックグラウンド スクリプトを生成するための API が定義されています。ウェブワーカーを使用すると、UI などのスクリプトをブロックすることなくユーザー操作を処理することなく、長時間実行スクリプトを起動して計算負荷の高いタスクを処理できます。誰もが使い慣れている、厄介な「応答しないスクリプト」というダイアログの解決に役立ちます。

「応答しないスクリプト」ダイアログ
一般的な応答しないスクリプト ダイアログ

ワーカーはスレッドのようなメッセージ受け渡しを利用して並列処理を行います。UI の更新、パフォーマンス、応答性を維持するのに最適です。

ウェブワーカーのタイプ

仕様では、専用ワーカー共有ワーカーの 2 種類のウェブワーカーについて説明しています。この記事では専用ワーカーについてのみ説明します。以下では、これらを「ウェブワーカー」または「ワーカー」と呼びます。

ご利用にあたって

ウェブワーカーは分離されたスレッドで実行されます。そのため、実行するコードは別のファイルに含める必要があります。 ただし、その前に、まずメインページで新しい Worker オブジェクトを作成します。コンストラクタはワーカー スクリプトの名前を使用します。

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

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

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

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

メッセージの受け渡しによるワーカーとの通信

処理と親ページの間の通信は、イベントモデルと postMessage() メソッドを使用して行います。ブラウザやバージョンに応じて、postMessage() は文字列または JSON オブジェクトを単一の引数として受け入れることができます。最新のブラウザの最新バージョンでは、JSON オブジェクトの受け渡しがサポートされています。

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

メイン スクリプト:

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() {
// 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);

転送可能なオブジェクト

ほとんどのブラウザは構造化クローン アルゴリズムを実装しているため、FileBlobArrayBuffer、JSON オブジェクトなど、より複雑な型を Worker の内外に渡すことができます。ただし、postMessage() を使用してこれらのタイプのデータを渡す場合は、引き続きコピーが作成されます。したがって、たとえば 50 MB の大きなファイルを渡す場合、そのファイルをワーカーとメインスレッドの間で取得する際に顕著なオーバーヘッドが発生します。

構造化クローンの作成は便利ですが、コピーの作成に数百ミリ秒かかることもあります。パフォーマンス ヒットに対処するには、転送可能なオブジェクトを使用できます。

転送可能オブジェクトを使用すると、データはあるコンテキストから別のコンテキストに転送されます。ゼロコピーであるため、Worker へのデータ送信のパフォーマンスが大幅に向上します。C/C++ の世界では、参照パスのようなものと考えてください。ただし、参照によるパスとは異なり、呼び出しコンテキストの「バージョン」は、新しいコンテキストに転送されると使用できなくなります。たとえば、ArrayBuffer をメインアプリから Worker に転送すると、元の ArrayBuffer は消去され、使用できなくなります。その内容が(文字どおり静かに)Worker コンテキストに転送されます。

転送可能オブジェクトを使用するには、少し異なる postMessage() のシグネチャを使用します。

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

ワーカーの場合、1 つ目の引数はデータ、2 つ目の引数は転送する必要があるアイテムのリストです。なお、最初の引数は ArrayBuffer である必要はありません。たとえば、次のような JSON オブジェクトを使用できます。

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

重要なのは、2 番目の引数が ArrayBuffer の配列であることです。これは譲渡可能なアイテムのリストです。

譲渡可能アイテムについて詳しくは、developer.chrome.com の投稿をご覧ください。

ワーカー環境

ワーカー スコープ

ワーカーのコンテキストでは、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;
...
};

ワーカーが利用できる機能

Web Worker はマルチスレッド動作のため、JavaScript の一部の機能にのみアクセスできます。

  • navigator オブジェクト
  • location オブジェクト(読み取り専用)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • アプリケーション キャッシュ
  • importScripts() メソッドを使用して外部スクリプトをインポートする
  • 他のウェブワーカーを発生させる

ワーカーは以下にアクセスできません。

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

外部スクリプトの読み込み

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

この例では、script1.jsscript2.js をワーカーに読み込みます。

worker.js:

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

これは、単一の import ステートメントとして記述することもできます。

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

サブワーカー

ワーカーは、子ワーカーを発生させることができます。これは、実行時に大規模なタスクをさらに分割する場合に適しています。ただし、サブワーカーにはいくつかの注意点があります。

  • サブワーカーは、親ページと同じオリジン内でホストする必要があります。
  • サブワーカー内の URI は、(メインページではなく)親ワーカーの位置を基準として解決されます。

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

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

インライン ワーカー

ワーカー スクリプトをすぐに作成したい場合や、個別のワーカー ファイルを作成せずに自己完結型のページを作成したい場合はどうすればよいでしょうか。Blob() を使用すると、ワーカーコードへの URL ハンドルを文字列として作成することで、メインロジックと同じ HTML ファイル内にワーカーを「インライン」できます。

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

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

blob の URL

そのためには、window.URL.createObjectURL() を呼び出す必要があります。このメソッドは、DOM File または Blob オブジェクトに格納されているデータを参照するために使用できる単純な URL 文字列を作成します。次に例を示します。

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

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

window.URL.revokeObjectURL(blobURL);

Chrome では、作成されたすべての blob URL を表示するページ(chrome://blob-internals/)があります。

完全な例

さらに一歩進めて、ワーカーの 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 blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
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 を使用して文字列として抽出され、Blob() に渡されてファイルを作成します。

外部スクリプトの読み込み

これらの手法を使用してワーカーコードをインライン化する場合、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(<b>{url: document.location}</b>);
</script>

エラーの処理

JavaScript のロジックと同様に、ウェブワーカーでスローされるすべてのエラーを処理します。ワーカーの実行中にエラーが発生した場合は、ErrorEvent が発生します。このインターフェースには、問題の原因を特定するために 3 つの有用なプロパティが含まれています。filename - エラーの原因となったワーカー スクリプトの名前、lineno - エラーが発生した行番号、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 は未定義です)。

// TODO: DevSite - インライン イベント ハンドラを使用したため、コードサンプルを削除する

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> での画像フィルタリング。
  • ローカル ウェブ データベースの多数の行を更新する場合。

Web Workers API に関するユースケースの詳細については、Workers の概要をご覧ください。

デモ

参照