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

Web Worker の 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 モジュールでサービス ワーカーをインスタンス化できるようになります。

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

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

その他のリソースと参考文献