Web Worker 實戰:如何在瀏覽器中安全卸載重計算任務
為什麼主執行緒會卡頓?
瀏覽器的主執行緒同時負責 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 的兩種方式
// 方式一:獨立檔案(推薦,便於程式碼分割)
const worker = new Worker(new URL('./pdf-worker.ts', import.meta.url));
// 方式二: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 檔案的打包與 hash。
通訊模式:postMessage 與 Transferable
結構化複製 vs 轉移所有權
// ❌ 拷貝:大 ArrayBuffer 會被完整複製,雙倍記憶體
worker.postMessage({ buffer: largeArrayBuffer });
// ✅ 轉移:零拷貝,主執行緒立即失去存取權
worker.postMessage({ buffer: largeArrayBuffer }, [largeArrayBuffer]);
對於 PDF 二進位資料(通常 1-50MB),Transferable 是必選項。工具庫的 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 關聯請求與回應,支援並行處理多個頁面。
工具庫中的 Worker 架構
PDF 轉圖片:多 Worker 並行
使用者上傳 PDF
↓
主執行緒:解析 PDF 頁數
↓
建立 Worker Pool(CPU 核心數 - 1)
↓
按頁分發渲染任務 → Worker 1, 2, 3, 4...
↓
主執行緒:收集結果 → 即時更新進度條
↓
打包 ZIP 下載
Worker 數量通常設為 navigator.hardwareConcurrency - 1,在並行度與記憶體佔用之間取得平衡。
FFmpeg.wasm:單 Worker + SharedArrayBuffer
影片轉碼使用 FFmpeg.wasm,它是一個大型 WASM 模組(~25MB)。由於 WASM 執行個體無法跨 Worker 共用,影片工具採用單 Worker 獨佔模式:
- 主執行緒:UI 狀態、進度展示、取消控制
- Worker:FFmpeg 轉碼、進度回呼
- 透過
SharedArrayBuffer共用進度資料(需 COOP/COEP 回應標頭)
錯誤處理與 Worker 生命週期
worker.onerror = (e) => {
console.error('Worker error:', e.message);
worker.terminate();
fallbackToMainThread(); // 降級到主執行緒
};
// 任務完成後及時終止,釋放記憶體
worker.terminate();
Worker 不會自動 GC——長時間執行的 Worker 會持續佔用記憶體。任務完成後應呼叫 terminate() 或在 Worker 內部主動釋放大型物件。
Worker 的限制與替代方案
| 限制 | 影響 | 替代方案 |
|---|---|---|
| 無法存取 DOM | 不能操作 UI | postMessage 回傳結果 |
| 無法存取 localStorage | 不能持久化 | IndexedDB(Worker 可存取) |
| 啟動開銷 ~50ms | 短任務不划算 | 任務 < 50ms 留在主執行緒 |
| 除錯困難 | DevTools 支援有限 | console.log + 獨立 Worker 面板 |
對於極短的任務(如 JSON 格式化 < 10KB),Worker 的啟動開銷可能超過計算本身。工具庫的策略是:檔案 > 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 將耗時從 18s 降至 5s,同時 UI 全程可回應——使用者可以取消任務、切換頁面、檢視已完成頁面的預覽。
最佳實務總結
- 按需啟用:短任務不必 Worker 化,避免啟動開銷
- Transferable 優先:大二進位資料必須轉移所有權
- Worker Pool:CPU 密集型任務用池化並行,I/O 密集型用單 Worker
- 及時終止:任務完成或取消後立即
terminate() - 降級策略:Worker 不可用時回退到主執行緒 +
requestIdleCallback - 進度回饋:長任務必須透過 postMessage 回傳進度,避免使用者以為頁面當機
Web Worker 是建構高效能瀏覽器工具的關鍵基礎設施。配合 WASM 與 Transferable Objects,它讓瀏覽器端處理 GB 級檔案成為可能——這正是工具庫 200+ 工具全部在瀏覽器本機執行的技術基石。
本站提供瀏覽器本地工具,免註冊即可試用 →