Web Worker 実践:ブラウザで重い計算を安全にオフロードする

技术架构(更新: 2026年5月19日)

なぜメインスレッドがカクつくのか?

ブラウザのメインスレッドは、JavaScript の実行、DOM レンダリング、イベント処理、レイアウト計算を一手に担います。JS タスクが 50ms を超えると、スクロールの遅れ、クリック無反応、アニメーションのドロップなど、ユーザーが「重い」と感じます。

典型的な高負荷タスク:

シナリオ 所要時間 メインスレッドへの影響
PDF をページ単位で画像化 2–30s ページが完全にフリーズ
FFmpeg による動画変換 10–120s キャンセル不可、UI フィードバックなし
PNG の一括圧縮 1–10s 入力が反応しない
大きな JSON の整形 100–500ms 短いカクつき

Web Worker の本質的な価値は、計算集約タスクをメインスレッドから切り離し、UI を常に操作可能に保つことです。


Web Worker の基本アーキテクチャ

スレッドモデル

┌─────────────────────────────────┐
│         Main Thread             │
│  DOM · Event Loop · Rendering   │
│         ↕ postMessage           │
├─────────────────────────────────┤
│         Web Worker              │
│  独立した JS コンテキスト・DOM 不可 │
│  独立した Event Loop            │
└─────────────────────────────────┘

Worker は独立した JavaScript 実行環境を持ちますが、DOM・window・document にはアクセスできません。データは postMessage でやり取りし、大きなオブジェクトは Transferable Objects でゼロコピー転送します。

Worker の作り方(2 通り)

// 方法1:別ファイル(推奨・コード分割に有利)
const worker = new Worker(new URL('./pdf-worker.ts', import.meta.url));

// 方法2:Blob URL(動的生成向け)
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

Next.js / Vite では new URL(..., import.meta.url) が推奨され、ビルドツールが Worker ファイルのバンドルとハッシュを処理します。


通信:postMessage と Transferable

構造化クローン vs 所有権の転送

// ❌ コピー:大きな ArrayBuffer が丸ごと複製され、メモリが倍になる
worker.postMessage({ buffer: largeArrayBuffer });

// ✅ 転送:ゼロコピー。メインスレッドは直ちにアクセス権を失う
worker.postMessage({ buffer: largeArrayBuffer }, [largeArrayBuffer]);

PDF のバイナリ(多くは 1–50MB)では Transferable が必須です。ToolsKu の PDF→画像では、100 ページ PDF で Transferable により 200MB 超のコピーを回避しています。

リクエスト・レスポンスパターン

// main.ts
const id = crypto.randomUUID();
worker.postMessage({ type: 'render', id, pageIndex: 0, pdfBytes }, [pdfBytes]);

worker.onmessage = (e) => {
  if (e.data.id === id) {
    const imageBlob = e.data.result;
    displayPreview(imageBlob);
  }
};

UUID でリクエストとレスポンスを対応づけ、複数ページを並列処理できます。


ToolsKu における Worker アーキテクチャ

PDF→画像:複数 Worker の並列化

ユーザーが PDF をアップロード
     ↓
メインスレッド:ページ数を解析
     ↓
Worker プールを作成(CPU コア数 − 1)
     ↓
ページごとにレンダリングを分配 → Worker 1, 2, 3, 4...
     ↓
メインスレッド:結果を集約 → 進捗バーを更新
     ↓
ZIP でダウンロード

Worker 数は通常 navigator.hardwareConcurrency - 1 とし、並列度とメモリ使用量のバランスを取ります。

FFmpeg.wasm:単一 Worker + SharedArrayBuffer

動画変換は FFmpeg.wasm(約 25MB の大型 WASM)を使用します。WASM インスタンスは Worker 間で共有できないため、動画ツールは 単一 Worker 専有 です:

  • メインスレッド:UI 状態、進捗表示、キャンセル
  • Worker:FFmpeg 変換、進捗コールバック
  • SharedArrayBuffer で進捗を共有(COOP/COEP ヘッダーが必要)

エラー処理と Worker のライフサイクル

worker.onerror = (e) => {
  console.error('Worker error:', e.message);
  worker.terminate();
  fallbackToMainThread(); // メインスレッドへフォールバック
};

// 完了後は terminate してメモリを解放
worker.terminate();

Worker は自動 GC されません。長時間稼働する Worker はメモリを占有し続けます。タスク完了後は terminate() するか、Worker 内で大きなオブジェクトを明示的に解放してください。


制限と代替手段

制限 影響 代替
DOM 不可 UI を直接操作できない postMessage で結果を返す
localStorage 不可 永続化できない IndexedDB(Worker から利用可)
起動コスト ~50ms 短いタスクには不向き 50ms 未満はメインスレッドに残す
デバッグが難しい DevTools サポートが限定的 console.log + Worker パネル

極めて短い処理(10KB 未満の JSON 整形など)では、起動コストが計算時間を上回ることがあります。ToolsKu では ファイル > 1MB、または見積もり > 100ms のときだけ Worker を有効化 します。


性能測定

Chrome 130、M2 MacBook Pro、50 ページ PDF → PNG:

方式 合計時間 メインスレッドブロック 操作可能
メインスレッド同期 18.2s 18.2s
単一 Worker 17.8s 0s
4 Worker 並列 5.1s 0s

並列 Worker で 18 秒が 5 秒に短縮され、UI は常に応答します。キャンセル、ページ遷移、完了ページのプレビューが可能です。


ベストプラクティス

  1. 必要なときだけ:短いタスクは Worker 化しない
  2. Transferable 優先:大きなバイナリは所有権を転送
  3. Worker プール:CPU 集約はプール並列、I/O 集約は単一 Worker
  4. すぐ terminate:完了・キャンセル後に terminate()
  5. フォールバック:Worker 不可時はメインスレッド + requestIdleCallback
  6. 進捗通知:長時間タスクは postMessage で進捗を返し、フリーズと誤解させない

Web Worker は高性能ブラウザツールの基盤です。WASM と Transferable と組み合わせれば、ブラウザ上で GB 級ファイルの処理も現実的になります。ToolsKu の 200 以上のツールがすべてローカルで動く技術的支柱です。

ブラウザローカルツールを無料で試す →

#Web Worker#多线程#性能优化#浏览器#架构