少し前のバージョンですが、Chrome 69 より Web Worker から Web Worker を作れるようになりました。この機能は Nested Workers とも呼ばれています。

使用方法とユースケース

使い方は Window 上で Worker を作る場合と同じです。次のコードでは、Window から Parent Worker、Parent Worker から Child Worker を作り、postMessage() で Child Worker から Window までメッセージを送信します。

// index.html
const worker = new Worker('parent-worker.js');
const msgEvent = await new Promise(resolve => parent_worker.onmessage = resolve);
console.log(msgEvent.data);  // 'Hello from a nested worker!'

// parent-worker.js
const child_worker = new Worker('child-worker.js');
const msgEvent = await new Promise(resolve => child_worker.onmessage = resolve);
postMessage(msgEvent.data);

// child-worker.js
postMessage('Hello from a nested worker!');

Parent Worker を terminate すると、Child Worker 達も一緒に terminate されます。

// index.html
const worker = new Worker('parent-worker.js');
worker.terminate();  // The parent and its all desendant workers will be terminated.

// parent-worker.js
const child_worker1 = new Worker('child-worker.js');
const child_worker2 = new Worker('child-worker.js');

// child-worker.js
const grandchild_worker = new Worker('grandchild-worker.js');

現在 Nested Workers が可能なのは Dedicated Worker から Dedicated Worker を作る場合のみで、Shared Worker や Service Worker から Dedicated Worker を作ったり、逆に Dedicated Worker から Shared Worker や Service Worker を作ることはできません1。各 Worker の違いについては「JavaScript のスレッド並列実行環境 - 2. Web Worker - Worker の種類とプロセスモデル」を参照してください。

nested workers creation

Nested Workers が実装される前は、Dedicated Worker から新しく Dedicated Worker を起動したい場合は Window を経由して作る必要がありました。これは Window との postMessage() の分余計なオーバヘッドがかかり、かつ Worker 間で直接メッセージングをするには別途 MessageChannel による通信路を作る必要があるなど、使い勝手が良いものではありませんでした (see: “JavaScript のスレッド並列実行環境 - 3. スレッド間 (実行コンテキスト間) 通信”)。

Nested Workers によりこれらの問題が解決され、Dedicated Worker 内で Dedicated Worker を使うライブラリを埋め込みやすくなるなど、ユースケースも広がります。

nested workers creation via window

Web Worker を使う際の一般的な注意点として、Worker を作りすぎないように気をつける必要があります。Worker を作るとネイティブスレッドが一つ作られ、さらにスレッドごとに V8 のインスタンスが生成されるため、むやみに new Worker() すべきではありません。最悪の場合、メモリ使用量の上限に引っかかって Out-Of-Memory (OOM) が発生し、sad タブ状態になります。スレッドプールを作ったり、不要な Worker は停止するなど、控えめに利用することをおすすめします (see: “JavaScript のスレッド並列実行環境 - 5. Worker のコスト”)。

実装の裏話

実は Nested Workers は新しい仕様ではありません。Chromium のバグトラッカーには 8 年以上前からバグ (crbug.com/31666) が登録されており、Firefox では以前から利用可能でした。バグ番号が五桁であるところに歴史を感じます2

以前から Nested Workers を実装したいという気持ちはあったのですが、Blink には歴史的な経緯により「ワーカースレッドは常にメインスレッドから起動される」という実装制約があり、そのアーキテクチャの複雑さから長年実現できていませんでした。単純にその制約を外せばいいかというとそんなことはなく、ワーカスレッドの起動や停止に合わせて協調するその他のコンポーネントがちゃんと動くようにする必要があります。

私は 2017 年に Worklet の実装を行いつつ、Nested Workers を目指した Worker インフラの再設計を進めていました (see: “Worker infrastructure roadmap 2017 - Google Docs”)。しかし、その新設計の実装が一段落ついた段階でも Nested Workers が本当に実現可能か不透明な部分が多く、二の足を踏んでいる状態でした。

しかし 2018 年に入り、幸運にも一人のスーパーエンジニアが Worker チームに加わり、彼の抜群の実装力によってあっさり実現できてしまいました。その結果 Chrome 69 で無事ローンチすることができました。

まとめ

Chrome 69 より Dedicated Worker から Dedicated Worker が作れるようになり、Web Worker のユースケースが広がりました。もし Nested Workers を使っていておかしな挙動を見つけた場合は、是非 Chromium のバグトラッカーへ報告してください。

注釈

  1. Dedicated Worker から Shared Worker の生成や、Shared Worker から Dedicated Worker の生成は HTML 仕様では定義されていますが Chrome では実装されていません。同様に Dedicated Worker から Service Worker の登録も Service Worker 仕様では定義されていますが Chrome では実装されていません (crbug.com/371690)。一方、Service Worker から Dedicated Worker や Shared Worker の生成は仕様の議論が行われている段階です (see: “Allow workers & shared workers to be created within a service worker - whatwg/html”)。 

  2. 現在はもうすぐ六桁目を使い切りそうなところです。