Web Worker
ブラウザーのメインスレッドとは別のスレッドで JavaScript を実行し、UI のブロッキングを防ぐ仕組み
Web Worker とは
Web Worker は、ブラウザーのメインスレッドとは別のバックグラウンドスレッドで JavaScript を実行する仕組みである。CPU 負荷の高い処理 (画像処理、暗号化、大量データのソート) をメインスレッドから分離し、UI のフリーズを防ぐ。
メインスレッドの問題
ブラウザーはページの JavaScript の実行、イベント処理、レイアウトと描画を 1 本のメインスレッドで順番にこなしている。ここで長く戻ってこない関数を呼ぶと、その間に届いたクリックやスクロールは順番待ちのまま溜まり、画面も更新されない。メインスレッドを 50 ミリ秒以上占有するひと続きの処理は long task と呼ばれ、入力への反応が遅れる直接の原因になる。
メインスレッド: [UI 描画] [イベント処理] [重い計算...........] [UI 描画]
↑ この間 UI がフリーズ
Web Worker: [UI 描画] [イベント処理] [UI 描画] [UI 描画]
Worker スレッド: [重い計算...........]
↑ メインスレッドは自由
基本的な使い方
Worker は別ファイルのスクリプトとして起動し、postMessage() と onmessage の 2 つで会話する。以下は type: 'module' を付けて ES モジュールとして読み込む例である。この指定を省くと従来型のスクリプトとして読み込まれ、import 文が使えず importScripts() による同期読み込みになる。なお .ts をそのまま渡せるのは Vite や webpack がビルド時に変換する前提であり、ブラウザーが TypeScript を解釈するわけではない。
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.postMessage({ data: largeArray });
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// worker.ts
self.onmessage = (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
};
やり取りはコピーで行われる
postMessage() に渡した値は共有されない。構造化複製アルゴリズムで直列化され、受け取った側で複製として組み立て直される。Map や Set、Date、循環参照を含むオブジェクトも運べる代わりに、関数やクラスのメソッド、DOM ノードは複製できず DataCloneError になる。クラスのインスタンスを渡してもプロトタイプは失われ、届くのはデータだけの素のオブジェクトである。
見落としやすいのは複製の代金である。直列化は送り出す側のスレッドで走るため、数十 MB の配列をそのまま渡すと、重い計算を追い出したはずのメインスレッドが今度は直列化で止まる。Worker へ切り出したのに体感が変わらないときは、計算ではなく受け渡しが律速になっていないかを疑う。生データを送って結果だけ返す設計にするか、後述の所有権の移転を使う。
Worker の種類
同じ Worker という名前でも、誰と共有されるかと何のためにあるかが違う。
| 種類 | 共有される範囲 | 用途 |
|---|---|---|
| Dedicated Worker | 生成した 1 つのページのみ | CPU 負荷の高い処理 |
| Shared Worker | 同一オリジンの複数のページや iframe | タブ間で 1 つの接続や状態を共有 |
| Service Worker | 登録したスコープ配下の同一オリジン | 通信の仲介、キャッシュ、プッシュ通知 |
Shared Worker は Worker オブジェクトへ直接 postMessage() するのではなく、port (MessagePort) を通してやり取りし、Worker 側は onconnect で接続ごとのポートを受け取る。専用 Worker の内部から生成することはできない。主要なブラウザーで揃って使える状態になったのは新しく、2026 年 8 月時点の MDN では 2026 年 5 月以降に利用可能になった機能として扱われている。
Service Worker は計算を逃がすための仕組みではなく、ページと通信の間に立つ常駐スクリプトである。役割が違うため、詳しくは Service Worker を参照。
Worker の制限
Worker から見えるグローバルは window ではなく DedicatedWorkerGlobalScope で、document も window も存在しない。DOM はスレッドセーフに作られていないため、複数のスレッドから触れる経路を用意すると競合を避けられない。仕様は最初から DOM への経路を持たせないことでこの問題を回避しており、画面の更新はメッセージを受け取ったメインスレッド側の仕事になる。localStorage のような同期的な保存 API も使えないため、Worker からの永続化には非同期の IndexedDB を使う。
| できること | できないこと |
|---|---|
fetch | DOM 操作 (document, window) |
setTimeout / setInterval | alert, confirm |
IndexedDB | localStorage |
WebSocket | UI の直接更新 |
crypto | Cookie の読み書き (document.cookie) |
Transferable Objects
大きなデータを Worker に渡す場合、コピーではなく所有権を移転 (transfer) して高速化する。
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // transfer (コピーなし)
// buffer は使えなくなる (所有権が Worker に移転)
console.log(buffer.byteLength); // 0
移転できるのは決められた型だけで、ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas、ReadableStream などが対象である。ふつうのオブジェクトや配列は移転できず複製になる。Uint8Array のような型付き配列も移転の対象ではないため、worker.postMessage(view, [view.buffer]) のように裏にある ArrayBuffer を指定する。移転した側のバッファーは切り離され、byteLength は 0 になり、読み書きしようとすると例外になる。渡し切りの処理には向くが、同じバッファーを往復させたい場合は Worker から返すときも移転し直す必要がある。
使うべきケース
画像処理 (リサイズ、フィルター、WebP 変換)、データ処理 (CSV パース、大量データのソート)、暗号化 (ハッシュ計算、暗号化/復号)、構文解析 (Markdown → HTML、コードハイライト) が適している。
逆に向かないのは、処理そのものが短い場合と、渡すデータが計算量に比べて大きい場合である。Worker の起動にもメッセージの 1 往復にも時間がかかるため、数ミリ秒で終わる処理を切り出すと全体では遅くなる。判断は勘ではなく計測で行い、開発者ツールでその処理が 50 ミリ秒を超えてメインスレッドを占有していることを確かめてから切り出す。
Node.js の Worker Threads との違い
どちらもメッセージのやり取りが基本で、構造化複製と所有権の移転という仕組みも共通である。違うのは環境と使える道具である。Worker Threads は worker_threads モジュールから生成し、起動時に workerData で初期値を渡せるほか、ファイルシステムなど Node.js の API を使える。Web Worker から使えるのは fetch や IndexedDB といったブラウザー側の API である。モジュール形式は、Web Worker が既定では従来型のスクリプト (type: 'module' を指定すると ES モジュール)、Worker Threads が CommonJS と ES モジュールの両方に対応する。
メモリの共有はブラウザーでもできるが条件が付く。SharedArrayBuffer はメインスレッドと Worker が同じメモリ領域を参照する型で、ブラウザーでは安全なコンテキストかつ cross-origin isolated な状態、つまり Cross-Origin-Opener-Policy と Cross-Origin-Embedder-Policy を設定したページでのみ使える (crossOriginIsolated の値で判定できる)。共有した領域への変更が相手にいつ見えるかは保証されないため、読み書きの整合は Atomics で取る。この 2 つのヘッダーを立てると埋め込む外部リソースの側にも条件が付くので、外部のスクリプトや広告を載せたページでは採用しにくい。Node.js ではこのヘッダーの制約が無く、そのまま共有できる点が実務上の差になる。
理論と実装の両面から学ぶなら関連書籍が参考になる。
この記事は役に立ちましたか?
関連用語
Worker Threads
Node.js で CPU 集約的な処理をメインスレッドをブロックせずに別スレッドで実行する仕組み
並行処理 (Concurrency) とは - 並列処理 (Parallelism) との違い
Concurrency (並行処理) の意味を解説。複数のタスクを論理的に同時進行させる手法で、物理的に同時実行する並列処理 (Parallelism) とは区別される。Node.js / Go / Rust の並行処理モデルの違いも紹介。
goroutine
Go の軽量スレッドで、数十万規模の並行処理を低コストで実現する
プロセスとスレッドの違いとは - メモリ空間 / 生成コスト / 通信方法を比較
プロセスとスレッドの違いをメモリ空間 / 生成コスト / 通信方法の 3 軸で比較。Node.js での使い分け、Lambda の実行環境、データ競合とコンテナの PID 1 の落とし穴まで解説
スレッドプール
事前に生成したスレッドを再利用し、スレッド生成のオーバーヘッドを削減する並行処理パターン
Go
Google が開発したシンプルで高速なコンパイル言語で、goroutine による軽量な並行処理が特徴