JavaScript 是一門 single-threaded 的語言,這個設計在大多數情境下都能滿足需求:瀏覽器只服務單一使用者、Event Loop 處理掉了 I/O 等待,背後也有許多輔助執行緒在跑。但是當應用要在瀏覽器上做複雜計算(圖片處理、機器學習、加密…等)時,single-threaded 的限制就會浮現:在 Main Thread 上跑的耗時運算會讓頁面卡住、失去回應。
Web Worker 就是解開這項限制的關鍵 API。
什麼是 Web Worker#
Web Worker(Web Worker)是瀏覽器提供的 Web API,可以開出一條與 Main Thread(又稱 UI Thread)完全獨立的執行緒來跑 JavaScript。兩條 thread 不會互相阻塞,並可以透過 postMessage 與 onmessage 互相溝通。對於想做平行運算的場景,這是少數的合法手段。
Web Worker 的限制#
聽起來很神,但實際上 Web Worker 有不少限制:
- 不能存取 DOM、
document、window、parent等 UI 相關物件。 - Worker 與主程式採用 share-nothing 模型,必須透過 message 溝通。
- 同源限制:Worker 載入的腳本必須來自同源。
- 開 Worker 與通訊都會消耗 CPU 與記憶體,過度使用反而拖累效能。
- 訊息溝通預設是 deep copy,資料量大時會成為效能瓶頸。
這些限制其實是為了避免 multi-threading 容易產生的 race condition,並維持 thread safe。Worker 仍可以使用 XMLHttpRequest、setTimeout、setInterval 等基本 API。
基本使用方式#
下方以費氏數列為例,把運算放到 Worker 中執行:
// worker.js
const fibonacci = (n) => {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
};
self.onmessage = (e) => {
const result = fibonacci(e.data);
self.postMessage(result);
};// main.js
if (window.Worker) {
const worker = new Worker("./worker.js");
worker.onmessage = (e) => {
console.log("Result is...", e.data);
};
worker.onerror = () => {
console.log("error");
};
document.querySelector("#calculate").onclick = () => {
const number = document.querySelector("#number-input").value;
worker.postMessage(number);
};
}由於計算被搬到另一條 thread,即使輸入很大的數字也不會影響頁面渲染流程。
資料的所有權轉移#
postMessage 預設是 deep copy,要傳遞較大的二進位資料時可以改用 ownership transfer,避免複製成本:
const buffer = new Uint8Array(1024 * 1024 * 32); // 32MB
for (let i = 0; i < buffer.length; i += 1) {
buffer[i] = i;
}
worker.postMessage(buffer.buffer, [buffer.buffer]);轉移之後,原本的 buffer 在主程式端就不再可用。實作了 Transferable 介面的物件才能轉移,例如 ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas。
如果需要共享而非轉移,則可以考慮 SharedArrayBuffer,並搭配 Atomics 處理同步。
注意事項 SharedArrayBuffer(SharedArrayBuffer)因為 Spectre 等安全議題曾被多家瀏覽器停用,目前需要透過
cross-origin isolated等特殊設定才能啟用。
適合 Web Worker 的場景#
亂用 Worker 反而會讓效能更差。下列任務通常較適合:
- 真正需要大量運算的工作,例如複雜排序、索引建立。
- 機器學習模型推論、加解密。
- 圖片或影像處理。
- 遊戲開發、Web AR/VR。
- Long polling。
- 搭配 WebAssembly (Wasm) 執行高效能模組。
換句話說,與 UI 流程無關、又特別耗時的工作就是 Web Worker 的舞台,這同時也達成了關注點分離。
用 Comlink 簡化 Worker 開發#
原生 Web Worker 的訊息傳遞 API 寫起來不太直覺,在 React 等框架專案中尤其顯得突兀。Google 開發的 Comlink 是針對 Web Worker 的輕量封裝(約 1.1KB),可以讓我們以「呼叫類別與方法」的方式操作 Worker,而不必到處寫 postMessage,使程式碼更貼近一般物件操作。
與 Service Worker 與 Worklet 的關係#
除了 Web Worker,瀏覽器另外還有 Service Worker(Service Worker)與 Worklet(Worklet)兩種特殊執行緒:前者負責離線快取、推播等 PWA 功能;後者則用於繪製、音訊等專屬流程。它們都遵循相同的隔離原則,理解 Web Worker 之後再去看這兩者會輕鬆許多。
小結#
Main Thread 既要處理 UI、又要跑 JavaScript,當運算量大時容易出現卡頓。Web Worker 是在瀏覽器端使出「影分身」的方式,把高耗時的工作切出去,但通訊與初始化都有成本,需要評估後再使用。隨著瀏覽器持續強化,越來越多 Web Worker 的應用浮現,對前端工程師來說,掌握它能在效能與架構上多出許多可能性。