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

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

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

ウェブ プラットフォームでは、スレッドと並列処理の主なプリミティブは Web Workers API です。Worker は、オペレーティング システム スレッドの上に構築された軽量な抽象化であり、スレッド間通信用のメッセージ パッシング 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 年以上前から利用可能です。これは、Worker のブラウザ サポートが優れており、最適化されていることを意味しますが、JavaScript モジュールよりもはるかに古いことも意味します。ワーカーが設計された当時はモジュール システムがなかったため、ワーカーにコードを読み込んでスクリプトを構成する API は、2009 年に一般的だった同期スクリプト読み込みアプローチと似たままになっています。

履歴: クラシック ワーカー

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

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

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

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 モジュールであるため、import ステートメントと export ステートメントを使用できます。すべての 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 グローバルが常に存在していました。サービス ワーカーを含むすべてのタイプのワーカーと 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 オプションが含まれます。

サービス ワーカーについてはどうでしょうか?

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

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

仕様が更新されたため、ブラウザは新しい動作の実装を開始しています。JavaScript モジュールをサービス ワーカーに導入する際には、いくつかの複雑な問題が伴うため、時間がかかります。サービス ワーカーの登録では、更新をトリガーするかどうかを判断する際に、インポートされたスクリプトを以前のキャッシュ バージョンと比較する必要があります。これは、サービス ワーカーに使用される JavaScript モジュールで実装する必要があります。また、サービス ワーカーは、更新を確認する際に、特定のケースでスクリプトのキャッシュをバイパスできる必要があります。

その他のリソースとその他の資料