モジュール ワーカーによるウェブのスレッド化

ウェブ ワーカーに JavaScript モジュールを使用することで、負荷の大きい処理をバックグラウンド スレッドに簡単に移動できるようになりました。

JavaScript はシングルスレッドなので、一度に 1 つのオペレーションしか実行できません。これは直感的で、ウェブ上の多くのケースで適切に機能しますが、データ処理、解析、計算、分析などの負荷の高いタスクを行う必要がある場合に問題になることがあります。ウェブで配信されるアプリケーションの複雑さが増すにつれて、マルチスレッド処理の必要性が高まっています。

ウェブ プラットフォームでは、スレッド化と並列処理のメインプリミティブが Web Workers API です。ワーカーは、オペレーティング システム スレッドの上の軽量の抽象化であり、スレッド間通信用のメッセージ受け渡し API を公開します。これは、コストの高い計算を行う場合や、大規模なデータセットを操作する場合に非常に便利です。これにより、メインスレッドをスムーズに実行しながら、1 つ以上のバックグラウンド スレッドでコストの高いオペレーションを実行できます。

次にワーカーの一般的な使用例を示します。ここでは、ワーカー スクリプトがメインスレッドからのメッセージをリッスンし、独自のメッセージを送信して応答します。

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Web Worker API は、ほとんどのブラウザで 10 年以上前から提供されています。つまり、ワーカーは優れたブラウザ サポートを備え、十分に最適化されていますが、JavaScript モジュールより古くなっています。ワーカーの設計時にモジュール システムが存在しなかったため、ワーカーにコードを読み込んでスクリプトを作成する API は、2009 年に一般的だった同期スクリプト読み込みアプローチと同様です。

履歴: 従来のワーカー

Worker コンストラクタは、ドキュメント URL に対する従来のスクリプト URL を受け取ります。このメソッドはすぐに新しいワーカー インスタンスへの参照を返します。これにより、メッセージング インターフェースと、ワーカーを直ちに停止して破棄する terminate() メソッドが公開されます。

const worker = new Worker('worker.js');

importScripts() 関数は、追加のコードを読み込むためにウェブワーカー内で使用できますが、各スクリプトを取得して評価するためにワーカーの実行を一時停止します。また、従来の <script> タグのようにグローバル スコープでスクリプトを実行します。つまり、あるスクリプトの変数を別のスクリプトの変数で上書きできます。

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

このため、従来からウェブ ワーカーはアプリケーションのアーキテクチャに多大な影響を及ぼしてきました。デベロッパーは、最新の開発手法をあきらめることなくウェブ ワーカーを使用できるように、巧妙なツールと回避策を作成する必要がありました。たとえば、webpack などのバンドラは、生成されたコードに小さなモジュール ローダの実装を埋め込み、コードの読み込みに importScripts() を使用しますが、変数の競合を回避し、依存関係のインポートとエクスポートをシミュレートするためにモジュールを関数でラップします。

モジュール ワーカーを入力

Chrome 80 には、JavaScript モジュールの人間工学とパフォーマンス上のメリットを備えた、ウェブ ワーカー向けの新しいモード、モジュール ワーカーが用意されています。Worker コンストラクタが新しい {type:"module"} オプションを受け入れるようになりました。このオプションは、<script type="module"> に合わせてスクリプトの読み込みと実行を変更します。

const worker = new Worker('worker.js', {
  type: 'module'
});

モジュール ワーカーは標準の JavaScript モジュールであるため、インポート ステートメントとエクスポート ステートメントを使用できます。すべての JavaScript モジュールと同様に、依存関係は特定のコンテキスト(メインスレッド、ワーカーなど)で 1 回だけ実行され、今後のすべてのインポートはすでに実行されているモジュール インスタンスを参照します。JavaScript モジュールの読み込みと実行はブラウザによっても最適化されます。モジュールの依存関係は、モジュールの実行前に読み込むことができます。これにより、モジュール ツリー全体を並行して読み込むことができます。モジュールの読み込みでは、解析されたコードもキャッシュに保存されます。つまり、メインスレッドとワーカーで使用されるモジュールは、1 回解析するだけで済みます。

JavaScript モジュールに移動すると、ワーカーの実行をブロックせずにコードの遅延読み込みを行う動的インポートを使用することもできます。動的インポートは、グローバル変数に依存するのではなく、インポートされたモジュールのエクスポートが返されるため、importScripts() を使用して依存関係を読み込むよりもはるかに明確です。

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

優れたパフォーマンスを確保するため、モジュール ワーカー内で古い importScripts() メソッドは使用できません。JavaScript モジュールを使用するようにワーカーを切り替えると、すべてのコードが厳格モードで読み込まれます。もう 1 つの注目すべき変更点は、JavaScript モジュールの最上位スコープの this の値が undefined であるのに対し、従来のワーカーでは、ワーカーのグローバル スコープである点です。幸いなことに、グローバル スコープへの参照を提供する self グローバルが常に存在しています。Service Worker だけでなく、DOM においても、あらゆるタイプのワーカーで利用できます。

modulepreload を使用してワーカーをプリロードする

モジュール ワーカーによるパフォーマンスの大幅な改善の 1 つは、ワーカーとその依存関係をプリロードできる機能です。モジュール ワーカーでは、スクリプトは標準の JavaScript モジュールとして読み込まれて実行されます。つまり、modulepreload を使用してプリロードすることも、事前に解析することもできます。

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

プリロードされたモジュールは、メインスレッドとモジュール ワーカーの両方でも使用できます。これは、両方のコンテキストでインポートされるモジュールの場合や、モジュールがメインスレッドとワーカーのどちらで使用されるかを事前に把握できない場合に役立ちます。

以前は、ウェブ ワーカー スクリプトをプリロードするために利用できるオプションは限られており、必ずしも信頼性がありませんでした。従来のワーカーにはプリロードのために独自の「ワーカー」リソースタイプがありましたが、ブラウザは <link rel="preload" as="worker"> を実装していませんでした。そのため、ウェブ ワーカーのプリロードに使用できる主な手法は <link rel="prefetch"> でした。これは完全に HTTP キャッシュに依存するようになりました。正しいキャッシュ ヘッダーと組み合わせて使用すると、ワーカーのインスタンス化でワーカー スクリプトのダウンロードを待つ必要がなくなりました。ただし、modulepreload とは異なり、この手法では依存関係のプリロードや事前解析がサポートされていませんでした。

共有ワーカーについて

Chrome 83 で、共有ワーカーが更新され、JavaScript モジュールのサポートが追加されました。専用ワーカーと同様に、{type:"module"} オプションを使用して共有ワーカーを作成すると、ワーカー スクリプトが従来のスクリプトではなくモジュールとして読み込まれるようになりました。

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

JavaScript モジュールをサポートする前は、SharedWorker() コンストラクタは URL とオプションの name 引数のみを想定していました。これは、従来の共有ワーカーの使用に対して引き続き機能しますが、モジュール共有ワーカーを作成するには、新しい options 引数を使用する必要があります。使用可能なオプションは、前の name 引数に優先する name オプションなど、専用ワーカーの場合と同じです。

Service Worker についてはどうでしょうか。

Service Worker の仕様は、モジュール ワーカーと同じ {type:"module"} オプションを使用して JavaScript モジュールをエントリ ポイントとして受け入れられるようにすでに更新されていますが、この変更はブラウザにまだ実装されていません。その後、次のコードを使用して JavaScript モジュールで Service Worker をインスタンス化できます。

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

このたび仕様が更新され、ブラウザでは新しい動作の実装が始めました。JavaScript モジュールを Service Worker に持ち込むことに伴い、さらに複雑な作業が必要になるため、時間がかかります。Service Worker の登録では、更新をトリガーするかどうかを判断する際に、インポートされたスクリプトと以前のキャッシュ バージョンを比較する必要があります。これは、Service Worker に使用する JavaScript モジュールに実装する必要があります。また、更新を確認する際、Service Worker でスクリプトのキャッシュをバイパスできる必要があります。

その他のリソースと関連情報