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仕様は50msを超えるタスクを「long task」と定義しており、応答性の指標INPはweb.devの基準で200ms以下が良好、500ms超が不良です。INPの測り方はCore Web Vitalsとは:LCP・INP・CLSの基準値とweb-vitals・CrUX APIでの測り方【2026年10月】で扱っています。
300msかかる計算を、メインスレッドとワーカーのそれぞれで実行し、その間にsetTimeout(0)のコールバックが何ms待たされたかを測りました。メインスレッドで計算した場合の最大待ち時間はChrome 156で301ms、Firefox 157で301ms、WebKit 27.2で307msと、計算時間がそのまま応答停止になっています。ワーカーに移すと最大待ち時間はChrome・Firefoxで約50ms、WebKitで15msに縮みました(ヘッドレス実行のためタイマーの間引きを含む値です)。
Web Workerは計算を速くするのではなく、計算中も入力を受け付けられる状態を保つ仕組みです。async/awaitは待ち時間を譲るだけでスレッドは増えないため、CPUを使い切る計算には効きません(非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説)。また描画とDOM更新はメインスレッドに戻るので、重いのが描画側なら効果は出ません。時間がどこに乗っているかはレンダリングとは?ブラウザ描画の仕組みとCSR/SSR/SSGの違い・選定基準を解説の描画パイプラインと照らして切り分けます。
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の公式ドキュメントが示す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による対応版はChrome 80・Firefox 114・Safari 15です。ワーカーの中でさらにワーカーを起こす入れ子構成は、Safariでは15.5から(入れ子でのスクリプト読み込みは16.4から)で、対象環境の下限が上がります。
クラシックワーカーのimportScriptsとインラインワーカー
typeを指定しないクラシックワーカーでは、importScripts('lib.js')で外部スクリプトを同期的に読み込みます。モジュールワーカーでこれを呼ぶと、HTML Standardの規定どおり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実装とWeb Locks APIとは?navigator.locks.requestの使い方と複数タブの排他制御【3ブラウザ実測】で解説しています。ワーカー内の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を使いますが、ページがクロスオリジン分離されている必要があります。レスポンスにCross-Origin-Opener-Policy: same-originと、Cross-Origin-Embedder-Policy: require-corpまたはcredentiallessを付けます(HTML Standard)。ヘッダーなしのページでは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)。この値を信じて台数を決めると環境によって過剰になるので、上の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分析を実行する仕組みと実装・採用判断のように、ワーカー前提で作られたライブラリはそのまま使うほうが早い。
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操作の実装で解説しています。業務システムでブラウザ側に数十万行の処理を持たせるなら、ワーカーへの分割を前提に画面とデータの流れを設計段階で決めておくと作り直しを避けられます。弊社のフルスクラッチ開発では、こうした設計から実装までを承っています。
「Worker」と付くAPIは3種類あり、寿命と役割が違います。
| 種別 | スコープ | 通信 | 寿命 | 主な用途 |
|---|---|---|---|---|
| Dedicated Worker | 生成したページ専用 | worker.postMessage | ページを閉じるまで | 重い計算の移し先 |
| SharedWorker | 同一オリジンの複数タブ | port.postMessage | 接続タブが全て閉じるまで | タブ間の接続共有 |
| Service Worker | 登録したスコープ配下 | fetch・messageイベント | イベントごとに起動・停止 | 通信の中継とキャッシュ |
一般に「Web Worker」と言えば1行目の専用ワーカーを指します。Service Workerはアイドル時にブラウザが停止させるため、長い計算の置き場所には向きません。登録とキャッシュの実装はPWAとは?Service Workerとマニフェストの実装手順・ネイティブアプリとの使い分けと採用判断を解説を参照してください。
Safariはいったん削除した後、2022年のSafari 16.0で「WebKit now supports Shared Workers.」として再対応しました。Android版Chromeでは長く無効でしたが、Chrome 148のリリースノートに「SharedWorker is being re-enabled on Android」と記載され、2026年5月のStableで再有効化されています。同じ版で、接続タブが全て閉じた後もワーカーを生かすよう求めるextendedLifetime: trueも入りましたが、BCDではFirefoxとSafariは未対応です。Chrome 148のほかの変更はChrome 148とは?リリース日・ビルド番号と変更点、Prompt APIとChromeOS 148の要点にまとめています。
同じページを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 });
};
};
専用ワーカーで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月】で整理しています。
CSPのworker-srcとワーカーのセキュリティ制約
ワーカーのスクリプトは、ページと同じオリジンから読み込む必要があります。CSPを運用しているサイトではworker-srcディレクティブがワーカーの読み込み元を制御し、指定が無ければchild-src、script-src、default-srcの順に参照されます(CSP Level 3)。
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は、ワーカーの関数を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のWorkerはEventEmitterを継承し、worker.on('message')で受け取り、ワーカー内ではNode.jsのAPIの大部分を使えます。ブラウザのWorkerはEventTargetを継承し、onmessageで受け取ります。Node.jsの公式ドキュメントも、ワーカーはCPUを使う処理には有効でもI/O中心の処理にはあまり役立たないとしています。