---
title: "Web Workerとは？使い方とService Workerとの違い、送れるデータの制約を3ブラウザで実測"
url: "https://www.issoh.co.jp/tech/details/3444/"
published: 2024-09-02
updated: 2026-11-01
categories: ["JavaScript"]
publisher: "株式会社一創"
---

# Web Workerとは？使い方とService Workerとの違い、送れるデータの制約を3ブラウザで実測

Web Workerは、JavaScriptをページの描画とは別のスレッドで動かすブラウザ標準のAPIです（WebWorker、ウェブワーカーとも表記）。重い計算をスレッドごと切り離せる代わりに、DOMに触れられず、データは原則としてコピーか所有権の移動で渡します（メモリを共有する`SharedArrayBuffer`は条件付きの例外）。この記事では使い方の最小コード、送れるデータの境界、複数ワーカーでの並列化、Service Worker・SharedWorkerとの違いを、2026年11月1日にChrome 156・Firefox 157・WebKit 27.2（Playwright経由のヘッドレス実行。FirefoxとWebKitはPlaywright同梱のエンジン）で動かした結果とともに整理します。

## まとめ：Web Workerの要点と判断基準

- Web Workerは別スレッドでJavaScriptを動かす仕組みで、メインスレッドとは主に`postMessage`のメッセージでつながる（共有メモリは`SharedArrayBuffer`、タブ間の通知は`BroadcastChannel`も使える）。`document`・`window`・`localStorage`はワーカー内に存在しない。
- 使い方は`new Worker()`・`postMessage`・`onmessage`の3つ。`{ type: 'module' }`のモジュールワーカーはChrome 80・Firefox 114・Safari 15以降で、この形式では`importScripts()`が`TypeError`になる。
- 送れるのは構造化クローンできる値だけ。関数とDOMノードは`DataCloneError`、クラスインスタンスはメソッドを失う。256MBの`ArrayBuffer`はコピーで往復0.2〜1.1秒、転送なら1〜3.1msだった。
- 効くのはメインスレッドで入力への応答を妨げるCPU処理（50msを超えるlong taskが目安の1つ）。ワーカー8台のプールで素数計算は約7倍速くなったが、物理コア数を超えて16台にしても短縮は約7〜13%にとどまった。
- Service Workerは通信の中継役で、計算の置き場所ではない。複数タブで1つのワーカーを共有するSharedWorkerは、Android版ChromeでもChrome 148から使える。
- ワーカー内で起きた未処理のPromise拒否は、メインスレッドの`onerror`に届かない。ワーカー側で`unhandledrejection`を拾って送り返す実装が要る。

## Web Workerの仕組み：メインスレッドが止まる理由と別スレッドの効果

ブラウザのメインスレッドは、JavaScriptの実行・スタイル計算・レイアウト・描画・入力イベントの処理を1本で順番にこなします。ここで長い計算が走ると、その間に届いた操作は後回しになります。W3Cの[Long Tasks仕様](https://w3c.github.io/longtasks/)は50msを超えるタスクを「long task」と定義しており、応答性の指標INPは[web.devの基準](https://web.dev/articles/inp)で200ms以下が良好、500ms超が不良です。INPの測り方は[Core Web Vitalsとは：LCP・INP・CLSの基準値とweb-vitals・CrUX APIでの測り方【2026年10月】](/tech/details/18645/)で扱っています。

300msかかる計算を、メインスレッドとワーカーのそれぞれで実行し、その間に`setTimeout(0)`のコールバックが何ms待たされたかを測りました。メインスレッドで計算した場合の最大待ち時間はChrome 156で301ms、Firefox 157で301ms、WebKit 27.2で307msと、計算時間がそのまま応答停止になっています。ワーカーに移すと最大待ち時間はChrome・Firefoxで約50ms、WebKitで15msに縮みました（ヘッドレス実行のためタイマーの間引きを含む値です）。

Web Workerは計算を速くするのではなく、計算中も入力を受け付けられる状態を保つ仕組みです。async/awaitは待ち時間を譲るだけでスレッドは増えないため、CPUを使い切る計算には効きません（[非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)）。また描画とDOM更新はメインスレッドに戻るので、重いのが描画側なら効果は出ません。時間がどこに乗っているかは[レンダリングとは？ブラウザ描画の仕組みとCSR/SSR/SSGの違い・選定基準を解説](/tech/details/13578/)の描画パイプラインと照らして切り分けます。

## Web Workerの使い方：new WorkerからpostMessageの往復まで

ワーカーは別ファイルのスクリプトとして用意し、メインスレッドから生成します。以下は500万回のループをワーカー側で回し、結果だけを受け取る最小構成です。`import.meta.url`を使うので、`main.js`は`<script type="module">`で読み込み、ファイルはローカルでもHTTPサーバー経由で配信します（`file://`で開くと、Chrome 156では`SecurityError`、WebKit 27.2では`onerror`でワーカーが起動しませんでした。Firefox 157は起動しましたが、ブラウザ間でそろわないので頼らないでください）。

```
// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });

worker.onmessage = (e) => {
  console.log(e.data.acc); // 12499997500000
};
worker.onerror = (e) => {
  console.error(e.message, e.filename, e.lineno);
};

worker.postMessage({ cmd: 'sum', n: 5_000_000 });
```

```
// worker.js
self.onmessage = (e) => {
  const { cmd, n } = e.data;
  if (cmd !== 'sum') return;
  let acc = 0;
  for (let i = 0; i < n; i++) acc += i;
  self.postMessage({ cmd, acc });
};
```

`new Worker()`からワーカーの最初のメッセージが届くまでは、Chrome 156で33ms、Firefox 157で45ms、WebKit 27.2で25msでした。操作のたびに生成と破棄を繰り返すとこの時間が毎回乗るので、1つ作って使い回し、不要になったら`worker.terminate()`で止めます。`terminate()`は実行中の処理を待たずに止めるため、書きかけの状態をワーカー内に持たない設計にします。ワーカー自身が終わるときは`self.close()`です。

### モジュールワーカーとバンドラでのURL指定

`new Worker('./worker.js')`と文字列でパスを書くと、バンドラがワーカーをビルド対象として認識できず、本番で404になることがあります。[Viteの公式ドキュメント](https://vite.dev/guide/features#web-workers)が示す`new Worker(new URL('./worker.js', import.meta.url))`の形なら、バンドラが静的解析してワーカー用のファイルを出力します。Viteの`import MyWorker from './worker?worker'`という書き方はVite専用なので、ほかのバンドラへ移す可能性があるなら`new URL`の形にそろえます。

`{ type: 'module' }`を付けるとワーカー内で`import`文が使えます。MDNの[browser-compat-data](https://github.com/mdn/browser-compat-data)による対応版はChrome 80・Firefox 114・Safari 15です。ワーカーの中でさらにワーカーを起こす入れ子構成は、Safariでは15.5から（入れ子でのスクリプト読み込みは16.4から）で、対象環境の下限が上がります。

### クラシックワーカーのimportScriptsとインラインワーカー

`type`を指定しないクラシックワーカーでは、`importScripts('lib.js')`で外部スクリプトを同期的に読み込みます。モジュールワーカーでこれを呼ぶと、[HTML Standard](https://html.spec.whatwg.org/multipage/workers.html#importing-scripts-and-libraries)の規定どおり`TypeError`になります。実際のメッセージはブラウザごとに異なり、Chromeは`Module scripts don't support importScripts().`、WebKitは`importScripts cannot be used if worker type is "module"`でした。古いライブラリが`importScripts`前提なら、そのワーカーだけクラシックにします。

別ファイルを用意せず、文字列からワーカーを作ることもできます。Blob URLを使うインラインワーカーです。

```
const src = `self.onmessage = (e) => self.postMessage(e.data * 2);`;
const url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' }));
const worker = new Worker(url);
worker.onmessage = (e) => {
  console.log(e.data); // 42
  URL.revokeObjectURL(url);
};
worker.postMessage(21);
```

ライブラリ内部でワーカーを使うときの定番ですが、CSPで`blob:`を許可していないサイトでは起動しません（後述）。

### エラー処理：onerrorに届く例外と届かない例外

ワーカー内で同期的に投げた例外は、メインスレッドの`onerror`に`ErrorEvent`として届き、`message`・`filename`・`lineno`が取れます。注意が要るのは次の2つです。

- ワーカー内の**未処理のPromise拒否**は、3エンジンとも`onerror`を発火させなかった。`fetch`の失敗をawaitで投げ直すような非同期処理の失敗は、メインスレッドからは見えない。
- ワーカーのスクリプトが**404**のときは、`ErrorEvent`ではなく素の`Event`が届き、`message`は空になる。`e.message`だけをログに出す実装では原因が残らない。

非同期の失敗は、ワーカー側で`unhandledrejection`を拾ってメッセージとして送り返します。

```
// worker.js
self.addEventListener('unhandledrejection', (e) => {
  self.postMessage({ type: 'error', message: String(e.reason?.message ?? e.reason) });
});

self.onmessage = async () => {
  const res = await fetch('/data.json');
  if (!res.ok) throw new Error(`HTTP ${res.status}`); // onerrorには届かない
};
```

存在しないURLで試すと、3エンジンとも`{ type: 'error', message: 'HTTP 404' }`が届きました。クローンできない値を渡した場合は、`postMessage`の呼び出し自体がその場で例外を投げます。

## ワーカー内で使えるAPIと使えないAPI

モジュールワーカーの中で各APIの有無を調べた結果です。3エンジンで同じ結果でした。

| API                              | ワーカー内 | 代わりの手段       |
| -------------------------------- | ----- | ------------ |
| document・window                  | なし    | 結果をメインへ返して描画 |
| localStorage・sessionStorage      | なし    | IndexedDB    |
| alert                            | なし    | メインで表示       |
| fetch・XMLHttpRequest             | あり    | ー            |
| WebSocket                        | あり    | ー            |
| IndexedDB・Cache API              | あり    | ー            |
| OffscreenCanvas                  | あり    | ー            |
| requestAnimationFrame            | あり    | ー            |
| BroadcastChannel・navigator.locks | あり    | ー            |

見落としやすいのは`localStorage`で、ワーカーの永続データはIndexedDBに置きます。タブ間の通知と排他はワーカー側で完結でき、使い方は[BroadcastChannel APIとは？JavaScriptのタブ間通信の使い方・ブラウザ対応・React実装](/tech/details/9588/)と[Web Locks APIとは？navigator.locks.requestの使い方と複数タブの排他制御【3ブラウザ実測】](/tech/details/9734/)で解説しています。ワーカー内の`requestAnimationFrame`はChrome 69・Firefox 99・Safari 16.4からで、Chromeでは入れ子のワーカーで使えません（BCDの注記）。

## postMessageで送れるデータ：構造化クローンと転送

`postMessage`の引数は構造化クローンのアルゴリズムで複製されます。通常のオブジェクトは参照ではなくコピーが渡るため、メインスレッドとワーカーが同じオブジェクトを共有することはありません。例外は後述の`SharedArrayBuffer`で、受け取った側のオブジェクトは別でも、裏のメモリは共有されます。

### 関数・DOMノード・クラスインスタンスの扱い

`Map`・`Date`・`RegExp`・`TypedArray`はそのまま複製できます。関数を含むオブジェクトを送ると、Chrome 156では`DataCloneError: Failed to execute 'postMessage' on 'Worker': () => {} could not be cloned.`で失敗し、`document.body`のようなDOMノードも同じ`DataCloneError`になりました。クラスインスタンスは送れますが、受け取った側では`Object`のプレーンなオブジェクトになり、メソッドは`undefined`です。

処理の指示は`cmd: 'sum'`のような文字列と数値に落とし、クラスは受け取った側で組み立て直します。

### ArrayBufferの転送とコピーの実測コスト

`ArrayBuffer`は第2引数の転送リストに載せると、コピーではなく所有権の移動になります。移動後、送信側のバッファは長さ0になります。

```
const buf = new ArrayBuffer(1024);
worker.postMessage(buf, [buf]); // 第2引数が転送リスト
console.log(buf.byteLength);    // 0（送信側では使えなくなる）
```

差がどれだけ出るかを、256MB（268,435,456バイト）のバッファをワーカーへ送り、ワーカーが長さだけを返すまでの時間で、各2回測りました（バッファ自体は送り返していません）。

| 256MBの往復            | Chrome 156    | Firefox 157   | WebKit 27.2   |
| ------------------- | ------------- | ------------- | ------------- |
| コピー：postMessage呼び出し | 451〜465ms     | 195〜476ms     | 138〜489ms     |
| コピー：往復              | 990〜1,135ms   | 606〜925ms     | 194〜735ms     |
| 転送：往復               | 1.8〜3.1ms     | 1ms           | 1〜2ms         |
| 100万件の配列：往復         | 1,798〜2,102ms | 3,040〜3,203ms | 1,874〜2,552ms |

コピーでは`postMessage`を呼んだ時点でメインスレッドが数百ms止まります。`{ id, name, v }`のオブジェクト100万件はさらに重く、Firefoxでは呼び出しだけで1.6〜1.7秒でした。大きなデータは`TypedArray`に詰めて転送するか、ワーカー側で生成しないと、移したはずの待ち時間がメインスレッドに戻ってきます。

### SharedArrayBufferとAtomicsの前提条件

両スレッドで同じメモリを読み書きしたい場合は`SharedArrayBuffer`を使いますが、ページがクロスオリジン分離されている必要があります。レスポンスに`Cross-Origin-Opener-Policy: same-origin`と、`Cross-Origin-Embedder-Policy: require-corp`または`credentialless`を付けます（[HTML Standard](https://html.spec.whatwg.org/multipage/browsers.html#coep)）。ヘッダーなしのページでは3エンジンとも`SharedArrayBuffer`自体が`undefined`で、2つのヘッダーを付けると`crossOriginIsolated`がtrueになり、ワーカー側の`Atomics.add`の結果をメインスレッドから読めました。

`require-corp`では、クロスオリジンの画像やスクリプトは、CORPヘッダーを返すかCORSで許可されたものしか読み込めず、iframeは埋め込まれる側の文書もCOEPとCORPに対応している必要があります。`credentialless`なら緩和されますが、BCDではChrome 96・Firefox 119からでSafariは未対応です。広告タグや外部埋め込みの多いページでは、この条件を満たせるかを先に確認します。

## 複数ワーカーで並列処理する：ワーカープールの実装と台数

1つのワーカーは1つのコアしか使いません。大きな計算を分割して複数のワーカーへ配れば、コア数ぶん並列に進みます。ワーカーを先に作っておき、空いたものに仕事を割り当てる最小のプールです。Viteなどのバンドラは`new Worker(new URL(...))`の形を見てワーカーを検出するので、URLではなくワーカーを作る関数を受け取ります。起動失敗からの復旧や、終了時に未完了のPromiseを片付ける処理は省いた正常系の例です。

```
// pool.js
export function createPool(create, size = Math.min(navigator.hardwareConcurrency || 4, 8)) {
  const all = Array.from({ length: size }, () => create());
  const idle = [...all], queue = [];
  const run = (w, job) => {
    w.onmessage = (e) => { job.resolve(e.data); next(w); };
    w.onerror = (e) => { e.preventDefault(); job.reject(new Error(e.message)); next(w); };
    try {
      w.postMessage(job.data);
    } catch (err) { // DataCloneErrorなどは同期で投げられる
      job.reject(err);
      next(w);
    }
  };
  const next = (w) => {
    const job = queue.shift();
    job ? run(w, job) : idle.push(w);
  };
  return {
    exec: (data) => new Promise((resolve, reject) => {
      const job = { data, resolve, reject };
      const w = idle.pop();
      w ? run(w, job) : queue.push(job);
    }),
    terminate: () => all.forEach((w) => w.terminate()),
  };
}
```

```
// main.js：2,000万までの素数を数える範囲を32分割して配る
import { createPool } from './pool.js';

const pool = createPool(
  () => new Worker(new URL('./prime.js', import.meta.url), { type: 'module' }),
  8
);
const N = 20_000_000, chunks = 32, step = Math.ceil(N / chunks);
const parts = await Promise.all(
  Array.from({ length: chunks }, (_, i) =>
    pool.exec({ from: i * step, to: Math.min(N, (i + 1) * step) }))
);
console.log(parts.reduce((a, b) => a + b, 0)); // 1270607
pool.terminate();
```

```
// prime.js：受け取った範囲の素数を試し割りで数える
self.onmessage = (e) => {
  const { from, to } = e.data;
  let count = 0;
  for (let n = Math.max(2, from); n < to; n++) {
    let prime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) { prime = false; break; }
    }
    if (prime) count++;
  }
  self.postMessage(count);
};
```

論理16コア・物理8コアのIntel Macで、台数を変えて実行した結果です。分割数は台数の4倍（1台のときは分割なし）にし、各条件1回の計測なので、台数の効果を見る目安として読んでください。

| ワーカー数 | Chrome 156 | Firefox 157 | WebKit 27.2 |
| ----- | ---------- | ----------- | ----------- |
| 1台    | 51.0秒      | 58.2秒       | 17.5秒       |
| 4台    | 14.3秒      | 15.3秒       | 5.8秒        |
| 8台    | 7.4秒       | 8.3秒        | 2.5秒        |
| 16台   | 6.8秒       | 7.8秒        | 2.2秒        |

この計算では、1台に対して8台でChrome 6.9倍・Firefox 7.0倍・WebKit 6.9倍速くなり、物理コア数を超えた16台では8台比でChrome 8.0%・Firefox 6.8%・WebKit 13.4%の短縮にとどまりました。各ワーカーは独立したJavaScriptの実行環境とヒープを持つので、台数を増やすとメモリ使用量も増えます。実際の増え方はブラウザのタスクマネージャーで確かめます。上限は物理コア数を目安にし、メインスレッドの分を1つ残す設計が無難です。

台数の基準に使う`navigator.hardwareConcurrency`は、ブラウザ間で値がそろいません。同じMacでFirefox 157は16、Chrome 156とWebKit 27.2は8を返しました。SafariはBCDの注記によるとフィンガープリント対策で4か8に丸めています（[WebKit bug 233381](https://webkit.org/b/233381)）。この値を信じて台数を決めると環境によって過剰になるので、上の`createPool`のように上限を固定値で抑えておきます。

## Web Workerの用途：パース・画像処理・描画・WebAssembly

ワーカーに向くのは、入力と出力が値で表せて、メインスレッドで動かすと入力への応答が遅れる処理です。50msを超えるlong taskは分かりやすい目安ですが、それより短くてもスクロール中やアニメーション中に頻繁に走る処理は対象になります。実務で効果が出やすいのは次の処理です。

- **CSV・JSONの大きなファイルのパース**：数十MBの`JSON.parse`や行の集計をワーカーで済ませ、表示に必要な行だけを返す。`fetch`もワーカー内で呼べるので、取得から集計までを任せられる。
- **画像・音声のピクセル処理**：`ImageData`の`data.buffer`を転送で渡し、フィルタをかけて転送で戻す。コピーを挟まないことが前提。
- **Canvasの描画**：`transferControlToOffscreen()`でCanvasの制御をワーカーへ渡すと、ワーカー内で描画命令を出せる。BCDの対応版はChrome 69・Firefox 105・Safari 16.4。
- **WebAssemblyの実行**：SQLエンジンや画像コーデックをWasmにしてワーカー上で動かす。[DuckDB WASMとは？ブラウザでSQL分析を実行する仕組みと実装・採用判断](/tech/details/15379/)のように、ワーカー前提で作られたライブラリはそのまま使うほうが早い。

OffscreenCanvasは、Canvasを転送リストに載せてワーカーへ渡します。

```
// main.js
const off = document.querySelector('canvas').transferControlToOffscreen();
const worker = new Worker(new URL('./draw.js', import.meta.url));
worker.postMessage({ canvas: off }, [off]);

// draw.js
self.onmessage = (e) => {
  const ctx = e.data.canvas.getContext('2d');
  ctx.fillStyle = 'rgb(255,0,0)';
  ctx.fillRect(0, 0, 64, 64);
};
```

WasmのモジュールとJavaScriptのやり取りは[WebAssembly(Wasm)とは？JavaScript連携とMemory・Table操作の実装](/tech/details/4250/)で解説しています。業務システムでブラウザ側に数十万行の処理を持たせるなら、ワーカーへの分割を前提に画面とデータの流れを設計段階で決めておくと作り直しを避けられます。弊社の[フルスクラッチ開発](/service/system/fullscratch/)では、こうした設計から実装までを承っています。

## Service Worker・SharedWorkerとの違いと選び分け

「Worker」と付くAPIは3種類あり、寿命と役割が違います。

| 種別               | スコープ        | 通信                 | 寿命           | 主な用途        |
| ---------------- | ----------- | ------------------ | ------------ | ----------- |
| Dedicated Worker | 生成したページ専用   | worker.postMessage | ページを閉じるまで    | 重い計算の移し先    |
| SharedWorker     | 同一オリジンの複数タブ | port.postMessage   | 接続タブが全て閉じるまで | タブ間の接続共有    |
| Service Worker   | 登録したスコープ配下  | fetch・messageイベント  | イベントごとに起動・停止 | 通信の中継とキャッシュ |

一般に「Web Worker」と言えば1行目の専用ワーカーを指します。Service Workerはアイドル時にブラウザが停止させるため、長い計算の置き場所には向きません。登録とキャッシュの実装は[PWAとは？Service Workerとマニフェストの実装手順・ネイティブアプリとの使い分けと採用判断を解説](/tech/details/13214/)を参照してください。

### SharedWorkerの対応状況とフォールバック

Safariはいったん削除した後、2022年の[Safari 16.0](https://webkit.org/blog/13152/webkit-features-in-safari-16-0/)で「WebKit now supports Shared Workers.」として再対応しました。Android版Chromeでは長く無効でしたが、[Chrome 148のリリースノート](https://developer.chrome.com/release-notes/148)に「SharedWorker is being re-enabled on Android」と記載され、2026年5月のStableで再有効化されています。同じ版で、接続タブが全て閉じた後もワーカーを生かすよう求める`extendedLifetime: true`も入りましたが、BCDではFirefoxとSafariは未対応です。Chrome 148のほかの変更は[Chrome 148とは？リリース日・ビルド番号と変更点、Prompt APIとChromeOS 148の要点](/tech/details/12012/)にまとめています。

同じページを2つ開いて接続すると、3エンジンとも接続数が1、2と増え、1つのインスタンスを共有していました。Android版Chrome 147以前など`SharedWorker`が未定義の環境は残るので、機能検出して専用ワーカーに落とします。`port`と`Worker`はどちらも`postMessage`と`onmessage`を持つため、呼び出し側は共通にできます。

```
const endpoint =
  typeof SharedWorker !== 'undefined'
    ? new SharedWorker(new URL('./shared-sum.js', import.meta.url), { type: 'module' }).port
    : new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });

endpoint.onmessage = (e) => console.log(e.data.acc); // 12499997500000
endpoint.start?.(); // MessagePortのときだけ必要
endpoint.postMessage({ cmd: 'sum', n: 5_000_000 });
```

```
// shared-sum.js：worker.jsと同じcmdをport経由で処理する
self.onconnect = (e) => {
  const port = e.ports[0];
  port.onmessage = ({ data: { cmd, n } }) => {
    if (cmd !== 'sum') return;
    let acc = 0;
    for (let i = 0; i < n; i++) acc += i;
    port.postMessage({ cmd, acc });
  };
};
```

### SharedWorkerによるWebSocket接続の共有

専用ワーカーでWebSocketを張ると、接続はタブの数だけ増えます。同じユーザーが5タブ開けば5接続です。SharedWorkerに置けば、同じオリジン・同じスクリプトURL・同じ`name`で生成したタブ同士は1つのインスタンスを共有するので、接続は1本になり、各タブは`port`経由で同じデータを受け取れます（別プロファイルやシークレットウィンドウとは共有されません）。

```
// shared.js
const ports = [];
let socket;

self.onconnect = (e) => {
  const port = e.ports[0];
  ports.push(port);
  port.onmessage = (ev) => {
    // タブのpagehideで送らせる。切断を検知しないとportが溜まり続ける
    if (ev.data?.cmd !== 'bye') return;
    const i = ports.indexOf(port);
    if (i !== -1) ports.splice(i, 1);
    port.close();
  };
  socket ??= connect();
};

function connect() {
  const ws = new WebSocket('wss://example.com/stream');
  ws.onmessage = (ev) => {
    const data = JSON.parse(ev.data);
    for (const p of ports) p.postMessage(data);
  };
  return ws;
}
```

JSONのパースもワーカー側で済みます。これは接続を共有する考え方を示す例で、切断時の再接続や、`pagehide`が届かずにタブが消えた場合の掃除は省いています。一方向の配信だけならSSEのほうが再接続の扱いが楽で、その比較は[SSEとWebSocketの違いと使い分け｜WebRTC・WebTransportまで性能比較【2026年10月】](/tech/details/4539/)で整理しています。

## CSPのworker-srcとワーカーのセキュリティ制約

ワーカーのスクリプトは、ページと同じオリジンから読み込む必要があります。CSPを運用しているサイトでは`worker-src`ディレクティブがワーカーの読み込み元を制御し、指定が無ければ`child-src`、`script-src`、`default-src`の順に参照されます（[CSP Level 3](https://www.w3.org/TR/CSP3/#directive-fallback-list)）。

`Content-Security-Policy: worker-src 'self'`を返すページで試すと、3エンジンとも同一オリジンのワーカーは動き、Blob URLのインラインワーカーは`onerror`で止まりました。インラインワーカーを使うライブラリを入れるなら`worker-src 'self' blob:`と明示します。`worker-src`と`child-src`が無く`script-src`に`blob:`が無い場合も同じく止まるので、本番だけ動かないときはまずCSPを疑います。

## Web Workerを使うべきでない場面

次の処理はワーカーに移すと遅くなるか、複雑になるだけです。

**データ量に対して計算が軽い処理**が最も典型的です。前述のとおり100万件のオブジェクトは送信から応答受信まで1.8〜3.2秒かかったので、数msのフィルタのためにワーカーへ渡すと逆効果になります。判断は、Performanceパネルで処理時間と実行頻度、入力への応答の遅れを測り、送受信にかかる時間と比べて行います。

**DOMやレイアウトの値に依存する処理**も移せません。要素の座標や計算済みスタイルはワーカーから読めないため、メインで測って送る往復が増えるだけです。

**数msの処理が大量に続くケース**は、一定件数ごとに`setTimeout`などで制御を返し、メインスレッドに入力を処理させるほうが単純です。DOMを更新しながら進める処理ならなおさらです。

## Comlinkによるワーカー通信のRPC化

`cmd`で分岐する`onmessage`は、処理が増えるとswitch文が膨らみます。GoogleChromeLabsの[Comlink](https://github.com/GoogleChromeLabs/comlink)は、ワーカーの関数をProxy経由で普通の関数呼び出しのように見せるライブラリです。npmのlatestは4.4.2（2024年11月7日公開）、依存パッケージは無く、ライセンスはApache-2.0です。

```
// worker.js
import * as Comlink from 'comlink';

Comlink.expose({
  sum(n) {
    let acc = 0;
    for (let i = 0; i < n; i++) acc += i;
    return acc;
  },
});
```

```
// main.js
import * as Comlink from 'comlink';

const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
const api = Comlink.wrap(worker);
const total = await api.sum(5_000_000); // Promiseが返る
```

コールバックを渡すときは`Comlink.proxy()`で包みます。素の`postMessage`なら`DataCloneError`になる関数を、Comlinkが参照のやり取りに変換します。SharedWorkerでは`Comlink.wrap(worker.port)`とportを渡します。ただし既定では引数と戻り値は複製され、呼び出しごとに1往復するので、ループの中で細かく呼ばず1回で大きな仕事を渡します。大きなバッファは`Comlink.transfer(value, [buffer])`で転送を指定できます。npmで最後に公開された版は2024年11月の4.4.2です。関数を1つ2つ呼ぶだけなら、`cmd`で分岐する自前のメッセージ処理でも足ります。

## DevToolsによるワーカーのデバッグと性能計測

Chrome DevToolsのSourcesパネルでは、実行中のワーカーがスレッドとして一覧に出て、選ぶとそのワーカーの中でブレークポイントとステップ実行が使えます。SharedWorkerは`chrome://inspect/#workers`から専用のDevToolsを開きます。ワーカーに`new Worker(url, { name: 'parser' })`と名前を付けておくと、一覧で見分けられ、ワーカー内でも`self.name`で参照できます。

効果はPerformanceパネルで確かめます。ワーカーは別トラックで表示され、メインスレッドから50ms超の帯が消えてワーカー側に移っていれば成功です。メインスレッドに`postMessage`の帯が長く残るなら、コピーのコストなので転送に切り替えます。

## よくある質問

### Web Workerとは何ですか？

ブラウザ上でJavaScriptを別スレッドで動かす標準APIです。ブラウザが実行環境をもう1つ立ち上げてスクリプトを実行し、メインスレッドとは`postMessage`で値をやり取りします。ワーカーの計算が長くても、画面は操作に応答し続けます。

### Service WorkerとWeb Workerの違いは何ですか？

Web Worker（専用ワーカー）は計算をページから切り離すもので、ページと寿命をともにします。Service Workerはページとネットワークの間に入るプロキシで、キャッシュ応答やプッシュ通知を担い、ページを閉じた後もイベントに応じて起動します。重い計算には専用ワーカーを使います。

### Web Workerに対応しているブラウザは？Safariでも使えますか？

専用ワーカーは、BCDの記録ではChrome 2・Firefox 3.5・Safari 4から対応しており、現行のブラウザで気にする必要はありません。確認が要るのは派生機能です。モジュールワーカーはChrome 80・Firefox 114・Safari 15から、SharedWorkerはSafari 16から、Android版ChromeのSharedWorkerはChrome 148からです。Safariでは`navigator.hardwareConcurrency`が4か8に丸められる点と、COEPの`credentialless`が未対応である点に注意します。

### Web WorkerからDOMを操作できますか？

できません。ワーカーには`document`も`window`も無いため、計算結果を返してDOMへの反映はメインスレッドで行います。Canvasへの描画だけなら、OffscreenCanvasをワーカーへ渡せばワーカー内で描画できます。

### Node.jsのworker\_threadsとWeb Workerは同じものですか？

考え方は同じですがAPIは別物です。[Node.jsのworker\_threads](https://nodejs.org/api/worker%5Fthreads.html)の`Worker`は`EventEmitter`を継承し、`worker.on('message')`で受け取り、ワーカー内ではNode.jsのAPIの大部分を使えます。ブラウザの`Worker`は`EventTarget`を継承し、`onmessage`で受け取ります。Node.jsの公式ドキュメントも、ワーカーはCPUを使う処理には有効でもI/O中心の処理にはあまり役立たないとしています。

## 関連記事

- [非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)
- [Core Web Vitalsとは：LCP・INP・CLSの基準値とweb-vitals・CrUX APIでの測り方【2026年10月】](/tech/details/18645/)
- [PWAとは？Service Workerとマニフェストの実装手順・ネイティブアプリとの使い分けと採用判断を解説](/tech/details/13214/)
- [BroadcastChannel APIとは？JavaScriptのタブ間通信の使い方・ブラウザ対応・React実装](/tech/details/9588/)
- [DuckDB WASMとは？ブラウザでSQL分析を実行する仕組みと実装・採用判断](/tech/details/15379/)

---

出典: [Web Workerとは？使い方とService Workerとの違い、送れるデータの制約を3ブラウザで実測](<https://www.issoh.co.jp/tech/details/3444/>)（株式会社一創）
