Web Workerのスレッドモデルとメッセージパッシング
重い処理でUIが固まる問題を、原理から解決できる。専用/共有/サービスの3種の分離、構造化複製によるメッセージ受け渡し、メモリ非共有を前提にした並行モデルを内部から正確に整理します。
- 1.Worker は別スレッドで動き、メインスレッドとメモリを共有しない。やり取りは postMessage による値の受け渡しだけで、変数や DOM を直接触ることはできない。
- 2.専用(Dedicated)は1ページ専属、共有(Shared)は同一オリジンの複数コンテキストが port 経由で共有、サービス(Service)はネットワークプロキシ用で寿命と役割がまったく違う。
- 3.メッセージは構造化複製でコピーされて渡る。Transferable はゼロコピー移動、SharedArrayBuffer だけが例外的に物理メモリを共有し、その同期には Atomics が必須になる。
なぜWorkerの「分離」を正確に理解する必要があるのか
JavaScript のメインスレッドは、スクリプト実行・DOM 操作・レイアウト・描画をすべて1本で回します。ここで重い計算を同期的に走らせるとイベントループが詰まり、クリックもスクロールも止まる——これが「UI が固まる」の正体です(基礎はイベントループの内部構造を参照)。Web Worker は計算を別スレッドへ逃がす仕組みですが、その設計思想を一言で言えば「メモリを共有しない並行モデル」です。スレッドを足しながらデータ競合(race condition)を原理的に避けるために、Worker は共有メモリではなくメッセージのコピー渡しを前提に設計されました。この前提を取り違えると、「Worker から DOM を触れない」「渡したオブジェクトが別物になっている」といった壁に必ずぶつかります。本記事はその分離モデルとメッセージパッシングの内部を解きほぐします。
メモリ非共有:なぜ変数もDOMも直接触れないのか
OS のネイティブスレッドは既定で同一プロセスのヒープを共有し、複数スレッドが同じメモリを同時に書き換えうるためロックが必須になります。Web Worker はこの逆を選びました。各 Worker は**独立した実行環境(agent/グローバルスコープ)**を持ち、メインスレッドの変数・関数・プロトタイプには一切アクセスできません。共有がなければ、そもそも同時書き換えが起きえないので、ロックなしで安全に並行実行できます。これが Worker モデルの中核にある設計判断です。
この帰結として、Worker からは DOM・window・document に触れません。DOM は単一スレッドからの逐次操作を前提に作られたツリーで、複数スレッドが同時にいじれば整合性が壊れるからです。Worker のグローバルは window ではなく WorkerGlobalScope(専用なら DedicatedWorkerGlobalScope)で、使えるのは fetch・WebSocket・setTimeout・Cache・IndexedDB などスレッド安全な範囲の API に限られます。
async/await や Promise は**同一スレッド上の並行(concurrency)で、待ち時間を譲り合うだけで CPU を1コアしか使いません。Worker は別スレッドでの並列(parallelism)**で、別コアを使い計算を物理的に同時進行させます。だから非同期化しても CPU バウンドな処理は速くならず、そこで初めて Worker が効きます。両者は競合せず役割が違います。
3種のWorker:分離の単位と寿命が違う
「Worker」と一括りにされがちですが、専用・共有・サービスの3種は誰と紐づき、どれだけ生きるかがまったく異なります。
| 種類 | 紐づく相手 | 通信方法 | 主な用途・寿命 |
|---|---|---|---|
| Dedicated(専用) | 生成した1つのドキュメント専属 | worker.postMessage / onmessage | 重い計算の隔離。親が閉じれば終了 |
| Shared(共有) | 同一オリジンの複数タブ/iframe で共有 | MessagePort(port.postMessage) | タブ間で1接続・1状態を共有。全利用者が閉じるまで生存 |
| Service | オリジン全体のネットワークプロキシ | MessagePort 経由(clients) | オフライン・キャッシュ。ページ非依存で起動/終了を繰り返す |
専用 Worker は最も単純で、new Worker('w.js') を呼んだそのページだけが所有します。ページが閉じれば Worker も破棄されます。共有 Worker は同一オリジンの複数のコンテキスト(タブ・iframe)から同じ1つのインスタンスへ接続でき、接続はそれぞれ MessagePort として表れます。タブ間で WebSocket 接続や状態を1本に束ねたいときに使います。Service Worker はそもそも計算隔離が目的ではなくネットワークプロキシで、ページとは独立に起動・終了を繰り返す特殊な存在です。ライフサイクルと制御権の詳細はService Workerのライフサイクルで扱っているとおり、上の2つとは寿命の考え方からして別物です。
専用 Worker は new Worker() のたびに別インスタンスが立ちます。一方、共有 Worker は同名スクリプト・同スコープなら接続が増えても実体は1つで、各接続が port になります。Service Worker も登録ごとに1つです。「タブごとに状態が増殖した」「逆に共有されてほしいのに別々になる」は、この単位の取り違えが原因です。
メッセージパッシング:コピーで渡る、参照では渡らない
Worker との唯一の通信路は postMessage と onmessage です。ここで決定的に重要なのは、渡すデータは参照ではなくコピーだということ。送信値は構造化複製(structured clone)アルゴリズムでシリアライズされ、受信側で別オブジェクトとして再構築されます。だから送信後に送信側で元オブジェクトを書き換えても、受信側には伝わりません。共有していないのですから当然です。
// main.js
const worker = new Worker('w.js');
const data = { nums: [1, 2, 3] };
worker.postMessage(data); // data はコピーされて渡る
data.nums.push(4); // この変更は Worker 側には届かない(別オブジェクト)
worker.onmessage = (e) => {
console.log('結果:', e.data); // 受信もコピー
};
// w.js(Worker 側)
self.onmessage = (e) => {
const sum = e.data.nums.reduce((a, b) => a + b, 0);
self.postMessage(sum); // 値を返す。これもコピーで戻る
};
構造化複製は Object/Array/Map/Set/Date/ArrayBuffer など広い型を扱い、循環参照も解決します。一方で関数・DOM ノード・Symbol は複製できず、含めると DataCloneError が投げられます。またプロトタイプや getter/setter は引き継がれません——複製されるのは自前の列挙可能プロパティの「値」だけで、結果はプレーンなオブジェクトになります(getter はその時点の戻り値が読まれ、ただのデータプロパティとして写ります)。「メソッドが消えた」「instanceof が偽になった」のはこのためです。
構造化複製はサイズに比例した時間でバイトを複製します。数十 MB の配列を毎フレーム postMessage で往復させると、計算を Worker に逃がしてもシリアライズ/デシリアライズで再びメインスレッドが詰まり、本末転倒になります。大きなバイナリは次節の Transferable か SharedArrayBuffer でコピーを避けるのが鉄則です。
ゼロコピーの逃げ道:TransferableとSharedArrayBuffer
コピーを避ける手段は2つあり、性質がまるで違います。
Transferable(転送可能オブジェクト)は、コピーせず所有権ごと移動します。postMessage の第2引数(転送リスト)に ArrayBuffer 等を渡すと、バイト列は複製されず移動先へ付け替えられ、移動元は detach されて使えなくなります(byteLength が0になる)。ArrayBuffer を被せた TypedArray ビューも巻き添えで無効化されます(背後のメモリが切り替わる原理はArrayBufferのメモリレイアウトと同じ)。ゼロコピーですが、片方しか持てない移動である点が要です。
const buf = new ArrayBuffer(64 * 1024 * 1024); // 64MB
worker.postMessage({ payload: buf }, [buf]); // 第2引数で転送(ゼロコピー)
buf.byteLength; // 0:移動元は detach され空になる
SharedArrayBuffer(SAB)は、唯一物理メモリそのものを複数スレッドで共有する型です。転送のような「片方が失う」移動ではなく、両スレッドが同じバイト列を同時に読み書きできます。これは Worker モデルの非共有原則の例外で、その代わりデータ競合が現実に起き得るため、読み書きの順序と可視性を保証する Atomics(Atomics.add・Atomics.wait・Atomics.notify 等)での同期が必須になります。SAB は強力ですが、Spectre 系攻撃の前提となったため、利用には Cross-Origin-Opener-Policy と Cross-Origin-Embedder-Policy による**クロスオリジン分離(cross-origin isolation)**が必要です。
| 手段 | メモリ | 渡した後の元データ | 同期の要否 |
|---|---|---|---|
| 構造化複製(既定) | コピー(独立) | そのまま使える | 不要(共有しない) |
| Transferable | 移動(ゼロコピー) | detach され無効化 | 不要(所有は片方だけ) |
| SharedArrayBuffer | 物理共有 | 両方が同じ実体を触り続ける | 必須(Atomics で同期) |
並行モデルとしての帰結
3つの渡し方は、そのまま3段階の「共有の強さ」になっています。コピーは最も安全だが大きいデータで遅く、転送は速いが片道、共有は最速だが自前同期が要る——この順で安全性と速度がトレードオフになります。多くの実務では、コントロール用の小さなメッセージはコピー、巨大バイナリ(画像・音声・WASM のヒープ)は転送、リアルタイムに双方向で叩き合う領域だけ SAB、と使い分けます。
postMessage はメッセージをキューに積むだけで即座に返り、受信側の onmessage は受信スレッドのイベントループで順番に処理されます。つまり1つの Worker 内では実行は逐次で、ハンドラの途中で別のメッセージが割り込むことはありません。Worker 内部にロックが要らないのはこのためで、並列性が欲しければ Worker の数を増やします(プール化)。
頻出は4点です。❶ Worker はメモリ非共有で DOM/window に触れず、通信は postMessage のみ。❷ 専用=1ページ専属、共有=同一オリジン複数コンテキストで port 共有、サービス=ネットワークプロキシ、と役割と寿命が別。❸ 既定の受け渡しは構造化複製によるコピーで、関数・DOM・prototype は写らない。❹ Transferable は所有権の移動(detach)、SharedArrayBuffer だけが物理共有で Atomics 同期が要る。コピー/移動/共有の三段を言い分けられるかが分岐点です。
まとめ
Web Worker の設計思想は「メモリを共有しない並行モデル」です。各 Worker は独立したグローバルスコープを持ち、メインスレッドの変数や DOM には触れず、通信は postMessage/onmessage に限られます。専用(1ページ専属)・共有(同一オリジンの複数コンテキストが MessagePort で共有する単一インスタンス)・サービス(ネットワークプロキシ)は、紐づく相手も寿命もまったく異なります。メッセージは既定で構造化複製によりコピーで渡るため、参照は共有されず関数・DOM・prototype は失われます。コピーが重いときは Transferable で所有権を移動(ゼロコピー、移動元は detach)し、真に共有メモリが要る場面だけ SharedArrayBuffer を使い、その代わり Atomics による同期とクロスオリジン分離を引き受けます。コピー/移動/共有という3段の選択を、安全性と速度のトレードオフとして設計に落とせれば、UI を止めずに計算を逃がすという Worker 本来の目的を正確に達成できます。
Web/フロントエンド Article
Web Workerのスレッドモデルとメッセージパッシングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Web Worker
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6
導入後に効く点
専用(Dedicated)は1ページ専属、共有(Shared)は同一オリジンの複数コンテキストが port 経由で共有、サービス(Service)はネットワークプロキシ用で寿命と役割がまったく違う。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Web Worker / 並行処理」に近いか確認する。
- 強みである「Worker は別スレッドで動き、メインスレッドとメモリを共有しない。やり取りは postMessage による値の受け渡しだけで、変数や DOM を直接触ることはできない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。