JavaScript – 效能與最佳化(Performance & Optimization)
主題:Web Worker
簡介
在前端開發中,我們常常會遇到 大量計算、資料處理 或 即時影像編碼 等需要耗費 CPU 的工作。若這些工作直接在主執行緒(main thread)執行,瀏覽器的 UI 會被阻塞,使用者會感受到卡頓、畫面不流暢,甚至出現「未回應」的情況。
Web Worker 正是為了解決這類問題而設計的。它允許我們把耗時的程式碼搬到 背景執行緒(background thread)中執行,主執行緒仍然可以保持對 DOM、事件與 UI 的即時回應。透過正確使用 Web Worker,我們可以在不犧牲使用者體驗的前提下,完成大量資料運算、加密/解密、圖像處理等工作。
本篇文章將從概念、實作、常見陷阱與最佳實踐,帶領讀者一步步掌握 Web Worker 的使用方式,並提供多個實用範例,協助你在實務專案中提升效能。
核心概念
1. 什麼是 Web Worker?
- 獨立的執行環境:Worker 只擁有自己的全域作用域 (
self),無法直接存取window、document、parent等 UI 相關物件。 - 非同步通訊:主執行緒與 Worker 之間只能透過 訊息傳遞(postMessage / onmessage) 來交換資料,資料會被序列化(structured clone)或拷貝(transferable objects)。
- 種類:
- Dedicated Worker:最常用的類型,僅與建立它的腳本有連結。
- Shared Worker:多個瀏覽器上下文(tab、iframe)可以共享同一個 Worker。
- Service Worker:主要用於離線快取與背景同步,屬於另一層面的 Worker。
重點:Web Worker 的主要目的是將 CPU 密集型任務離線化,而不是用來直接操作 DOM。
2. 建立與使用 Dedicated Worker
// main.js (主執行緒)
const worker = new Worker('worker.js'); // 建立 Worker,傳入檔案路徑
// 傳送資料給 Worker
worker.postMessage({ cmd: 'fib', n: 40 });
// 接收 Worker 回傳的結果
worker.onmessage = (event) => {
console.log('Fibonacci result:', event.data);
};
// 若發生錯誤
worker.onerror = (err) => {
console.error('Worker error:', err);
};
// worker.js (Worker 執行緒)
self.onmessage = (event) => {
const { cmd, n } = event.data;
if (cmd === 'fib') {
const result = fibonacci(n);
// 回傳結果給主執行緒
self.postMessage(result);
}
};
// 一個簡單的遞迴 Fibonacci(僅作示範,實務上建議使用迭代法)
function fibonacci(num) {
if (num <= 1) return num;
return fibonacci(num - 1) + fibonacci(num - 2);
}
說明:
new Worker('worker.js')會在背景執行緒載入worker.js,此檔案必須 同源(或透過 CORS 設定)。postMessage與onmessage是雙向通道,資料在傳遞時會自動 structured clone,不會共享記憶體。
3. Transferable Objects – 零拷貝傳遞
對於大量二進位資料(如 ArrayBuffer、MessagePort),使用 Transferable Objects 可以避免拷貝,直接把所有權轉移給對方。
// 主執行緒
const buffer = new ArrayBuffer(10 * 1024 * 1024); // 10 MB
worker.postMessage(buffer, [buffer]); // 第二個參數是 transfer list
console.log(buffer.byteLength); // 0,所有權已被轉移
// worker.js
self.onmessage = (e) => {
const receivedBuffer = e.data; // 已取得所有權
console.log(receivedBuffer.byteLength); // 10 MB
// ... 進行計算或解碼
// 完成後若要回傳,可再次使用 transfer
self.postMessage(receivedBuffer, [receivedBuffer]);
};
重點:使用 Transferable Objects 時,原始物件在傳遞後會變成空的(byteLength 為 0),因此只能在一次傳遞中使用。
4. Shared Worker 的簡易範例
Shared Worker 允許多個頁面共享同一個執行緒,適合需要 跨頁面同步狀態 的情境(如即時聊天、多人協作)。
// shared-worker.js
let connections = [];
self.onconnect = (event) => {
const port = event.ports[0];
connections.push(port);
port.onmessage = (e) => {
// 廣播訊息給所有連線
connections.forEach(p => p.postMessage(e.data));
};
};
// main.js (任一頁面)
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.start(); // 必須啟動 port
sharedWorker.port.onmessage = (e) => {
console.log('收到廣播:', e.data);
};
// 發送訊息給其他頁面
sharedWorker.port.postMessage('Hello from page A');
5. 使用 Promise 包裝 Worker
為了讓程式碼更易讀,我們常把 postMessage 包裝成 Promise,讓呼叫者可以使用 async/await。
// worker-wrapper.js
export function runWorker(scriptUrl, payload) {
return new Promise((resolve, reject) => {
const worker = new Worker(scriptUrl);
worker.onmessage = (e) => {
resolve(e.data);
worker.terminate(); // 完成後釋放資源
};
worker.onerror = (e) => reject(e);
worker.postMessage(payload);
});
}
// 使用方式
import { runWorker } from './worker-wrapper.js';
async function calculatePrime(limit) {
const result = await runWorker('prime-worker.js', { limit });
console.log('Prime numbers:', result);
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 無法存取 DOM | Worker 沒有 document、window,直接操作 UI 會拋錯。 |
把 UI 相關工作留給主執行緒,透過 postMessage 讓 Worker 回傳結果再更新畫面。 |
| 過度建立 Worker | 每個 Worker 都會佔用一個執行緒,過多會造成記憶體與 CPU 飽和。 | 重用 Worker,或使用 Pool(工作池)管理固定數量的 Worker。 |
| 資料拷貝成本 | 若傳遞大量陣列或物件,會觸發深拷貝,降低效能。 | 使用 Transferable Objects(ArrayBuffer、MessageChannel)或 structured clone 只傳遞必要資料。 |
| 錯誤處理不足 | Worker 內部錯誤不會自動冒泡到主執行緒。 | 監聽 worker.onerror,在 Worker 程式內使用 try/catch 並 postMessage 錯誤資訊。 |
| 同源政策限制 | Worker 必須遵守同源(CORS)規則,跨域載入會失敗。 | 設定正確的 CORS 標頭,或使用 blob URL 方式動態產生腳本。 |
| 記憶體泄漏 | 未呼叫 worker.terminate(),Worker 會持續佔用資源。 |
在工作完成或不再需要時 立即 terminate,或使用 self.close() 於 Worker 端自行關閉。 |
最佳實踐小結
- 只把 CPU 密集型任務交給 Worker,避免把 I/O(如 fetch)搬到 Worker,因為主執行緒已能很好處理非阻塞 I/O。
- 使用模組化 Worker(
type: "module")可直接importES6 模組,提升維護性。
// main.js
const worker = new Worker('math-worker.js', { type: 'module' });
- 建立 Worker Pool:若需同時處理多筆任務,建立固定數量的 Worker,使用佇列(queue)分配工作。
- 盡量使用 Transferable Objects,尤其在處理影像、音訊、二進位檔案時,可減少記憶體拷貝。
- 監控效能:使用 Chrome DevTools 的「Performance」與「Workers」面板,觀察每個 Worker 的 CPU 使用率,避免單一 Worker 佔用過高資源。
實際應用場景
| 場景 | 為何適合使用 Web Worker |
|---|---|
| 大型陣列排序(如 10 萬筆資料) | 排序演算法是 CPU 密集型,搬到 Worker 可避免 UI 卡頓。 |
| 即時影像/影片編碼(WebRTC、Canvas) | 需要大量像素運算,使用 Worker 搭配 OffscreenCanvas 可在背景完成渲染。 |
| 加密/解密(AES、RSA) | 密碼學演算往往耗時,Worker 可讓使用者在等待時仍能操作介面。 |
| 資料分析與視覺化(大量統計、圖表生成) | 先在 Worker 完成計算,再把結果傳回主執行緒渲染圖表。 |
| 離線同步與背景下載(Progressive Web App) | 下載大檔案或同步大量資料時,使用 Worker 防止 UI 失去回應。 |
| 多人協作的即時狀態同步(Shared Worker) | 多個瀏覽器分頁共享同一個狀態(如聊天室訊息、遊戲分數)。 |
範例:使用 OffscreenCanvas 在 Worker 中渲染動畫
// main.js
const canvas = document.getElementById('myCanvas');
const offscreen = canvas.transferControlToOffscreen(); // 取得可轉移的 Canvas
const renderWorker = new Worker('render-worker.js');
renderWorker.postMessage({ canvas: offscreen }, [offscreen]); // 轉移所有權
// render-worker.js
self.onmessage = (e) => {
const { canvas } = e.data;
const ctx = canvas.getContext('2d');
let angle = 0;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(angle);
ctx.fillStyle = '#ff5722';
ctx.fillRect(-50, -50, 100, 100);
ctx.restore();
angle += 0.02;
requestAnimationFrame(draw);
}
draw();
};
此範例中,渲染工作完全在 Worker 中執行,即使畫面持續更新,主執行緒仍能保持流暢的 UI 互動。
總結
Web Worker 是前端效能優化的重要工具,透過將 CPU 密集型 任務搬到背景執行緒,我們可以:
- 避免 UI 卡頓,提升使用者體驗。
- 利用多核心(multi‑core)CPU,真正做到平行運算。
- 安全地傳遞大量資料,尤其配合 Transferable Objects 可達到零拷貝。
在實作時,記得 只在 Worker 中處理計算,使用訊息通道與主執行緒溝通,並遵守 同源政策、適時釋放資源(terminate / close)。同時,根據需求選擇 Dedicated、Shared 或 Service Worker,並結合 Worker Pool、Promise 包裝 等模式,讓程式碼更易維護、效能更佳。
透過本文的概念與範例,你已具備在實務專案中 合理使用 Web Worker 的基礎。未來不論是處理大量資料、即時影像或是跨分頁協作,都能運用這項技術,讓你的 JavaScript 應用變得更快、更穩定。祝開發順利! 🚀