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

ウェブワーカーの 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> タグと同様に、グローバル スコープでスクリプトを実行します。つまり、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 を使用してワーカーをプリロードする

モジュール ワーカーによる大幅なパフォーマンス改善の一つは、ワーカーとその依存関係をプリロードできることです。モジュール ワーカーを使用すると、スクリプトは標準の 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"> を実装したブラウザはありませんでした。その結果、ウェブワーカーのプリロードに使用できる主な手法は、HTTP キャッシュに完全に依存する <link rel="prefetch"> を使用することでした。適切なキャッシュ ヘッダーと組み合わせて使用すると、ワーカー スクリプトのダウンロードを待たずにワーカーをインスタンス化できるようになりました。ただし、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 モジュールを使用して Service Worker をインスタンス化できるようになります。

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

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

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