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

ウェブ ワーカーに 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 年以上前からほとんどのブラウザで利用可能でした。つまり、ワーカーは優れたブラウザ サポートと優れた最適化を備えていますが、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"> を実装していませんでした。そのため、ウェブワーカーをプリロードするために使用できる主な手法は、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 オプションなど、専用ワーカーの場合と同じです。

Service Worker はどうですか?

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

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

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

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