透過前篇文章可以發現 Web Workers 的強大,我們使用 Web Workers API 讓 JavaScript 可以在瀏覽器中並行執行任務,提升網站效能與使用者體驗。BUT!!!,大家都懂的後面的「但是」才是重點 XXD,儘管 Web Workers 如此厲害,他們還是存在一些限制,以下就跟大家分享 Web Workers 的限制,以及優化要注意的方向。
Web Workers 的主要限制之一是無法直接存取 DOM,因為 Web Workers 設計理念,是在和主執行緒並行的獨立執行緒中工作,目的當然就是避免阻塞主執行緒,也因為這個特性,他們沒辦法直接操作頁面上的元素或是修改 DOM。
但要較真的話,其實他的限制是 Web Workers 無法「直接」存取 DOM,因為我們還是能透過消息傳遞與主執行緒進行通訊,例如 Web Workers 可以透過postMessage()
方法將資料送到主執行緒,主執行緒再使用 onmessage
事件處理接收資料並更新 DOM。
<!-- 顯示計算結果 -->
<p id="result"></p>
在 main.js 建立一個 Web Worker,透過 onmessage
更新 DOM 元素
const worker = new Worker('worker.js');
// 設定接收來自 Worker 的消息
worker.onmessage = function(event) {
const data = event.data;
// 更新 DOM 元素
document.getElementById('result').textContent = `計算結果: ${data.result}`;
};
// 發送資料到 Worker
worker.postMessage({ number: 42 });
在 worker.js 處理從 main.js 得到的資料數據
// 設定接收來自主執行緒的消息
onmessage = function(event) {
const number = event.data.number;
const result = number * number;
// 將結果發送回主執行緒
postMessage({ result: result });
};
這種通訊方式雖然是間接的,但可以有效將 Web Workers 的計算與 DOM 操作分離,避免了我們因直接操作 DOM 而造成主執行緒的阻塞。
我們在用 Shared Workers 時有提到,多個主執行緒必須為同源才能互相通訊。這個限制是出於安全考量,目的是防止惡意代碼從不信任的來源載入到 Web Workers 中,從而保護使用者的安全。
同源政策提升了安全性,但在某些情況下,這些限制可能會帶來一些不便。例如,當需要從不同的來源動態載入腳本或進行跨源請求時,這種限制可能會妨礙應用的靈活性。儘管可以通過 CORS (跨源資源共享) 機制來允許跨源請求,但這種方法在 Web Workers 中的應用仍然受到限制,以下再簡單說明 CORS 在 Web Workers 應用的差異:
在 Workers 內部,可以使用 fetch
API 或 XMLHttpRequest
進行跨域請求,這些請求需要 Server 端正確設定 CORS 以允許不同來源的請求。
CORS 並不會影響 Worker 的同源限制,也就是說 Workers 的腳本本身必須來自與主頁面相同的來源,假設主頁面為 main.js
,腳本為 service-worker.js
,那這兩個檔案不在同個來源的話,即使 Server 端設定了 CORS,Workers 也無法載入該腳本,這就是 Web Workers 的限制。
最直接的方法,當然還是確保 Worker 腳本和主頁面在相同的來源,這是最不會有問題的。
如果要載入外部的腳本,可以考慮將腳本託管在與主頁面相同的來源下,或者使用 CDN 進行部署,確保所有資源都是同源
也可以考慮用 JSONP 或讓 Server 端代理處理資源的載入問題。
Web Workers 並不支援所有的 JavaScript API,像是 alert
、confirm
和 prompt
這些會跟使用者進行互動的 API,Web Workers 都不支援,因為它們無法在 Worker 執行緒中跳出對話框。此外,Web Workers 也不支援 localStorage
和 sessionStorage
等 Web Storage API,因為這些 API 依賴於存取 DOM 的能力,而我們在前面就提過,Web Workers 沒辦法直接存取 DOM。
因此,在使用 Web Workers 時必須考慮到它的特性,選擇適合的 API 進行開發,Web Workers 最重要的特色就是處理大量的資料數據以及網路請求,因此使用 fetch
和 XMLHttpRequest
等 API,會是能將 Web Workers 功能發揮到淋漓盡致的最好辦法。
在使用 Web Workers 時,將複雜且需密集計算的任務拆分為更小的子任務,可以避免單個 Worker overload,也能提高計算效率。
在 main.js 拆分複雜的任務,並發送給 Worker
const worker = new Worker('chunk.js');
worker.onmessage = function(event) {
const data = event.data;
console.log(`接收到來自 Worker 的結果:${data.result}`);
};
// 拆分複雜的任務並發送給 Worker
function processLargeData(data) {
// 假設我們要處理一個大的數據數組,並將其拆分為多個部分
const chunkSize = Math.ceil(data.length / 4);
for (let i = 0; i < 4; i++) {
const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize);
worker.postMessage({ chunk: chunk, index: i });
}
}
const largeData = [/* ... 大量數據 ... */];
processLargeData(largeData);
然後在 chunk.js 將收到的 chunk,透過 processChunk
函式處理,計算結果後再送回主執行緒。
// 設定接收來自主執行緒的消息
onmessage = function(event) {
const { chunk, index } = event.data;
// 處理每個子任務的數據
const result = processChunk(chunk);
// 將結果發送回主執行緒
postMessage({ result: result, index: index });
};
function processChunk(chunk) {
let sum = 0;
for (let i = 0; i < chunk.length; i++) {
sum += chunk[i];
}
return sum;
}
但我們要處理大量的數據資料,或需要頻繁的傳輸時,使用高效率的數據結構,可以減少序列化和反序列化的開銷,以下分享幾種高效的數據結構方式:
ArrayBuffer
和 TypedArray
大家對 ArrayBuffer
應該不陌生,我在前面的章節 [[File API 介紹與實際應用]] 也有簡單提過,他是低階的數據結構,用來表示原始二進制數據的固定長度緩衝區,他不直接操作數據,而是像容器一樣提供儲存空間。
TypedArray
則是用來在 ArrayBuffer
操作數據的 view,等等會用到的 Int32Array
就是 TypedArray
的一種。
// 建立 ArrayBuffer 以及 TypedArray
const buffer = new ArrayBuffer(16);
const intView = new Int32Array(buffer);
// 定義資料
intView[0] = 123;
intView[1] = 456;
// 讀取資料
console.log(intView[0]); // 123
SharedArrayBuffer
SharedArrayBuffer
是一種特殊的 ArrayBuffer
,可以在主執行緒和 Web Worker 之間共享。它提供了低延遲的數據交換方式,適合需要在多個執行緒間共享大量數據的場景。
// 主執行緒 (main.js)
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
// 發送 SharedArrayBuffer 到 Worker
const worker = new Worker('worker.js');
worker.postMessage(sharedBuffer);
// Worker 中 (worker.js)
onmessage = function(event) {
const sharedArray = new Int32Array(event.data);
// 修改共享數據
sharedArray[0] = 789;
};
看完這些數據結構後,大家可能會好奇,那如果用 JSON
傳遞數據呢?
如果是簡單的數據結構,我們的確可以用 JSON
減少轉換的開銷,但如果數據很大,或者數據是二進制,使用 ArrayBuffer
可能是個更好的選擇。
我們需要合理的使用 Worker,盡量重用已有的 Worker 實例,避免不斷建立新的實例;此外,在不需要用到 Worder 時,使用 terminate
方法終止 Worker,以釋放系統資源。
關於進一步的 Web Workers 生命週期介紹,我會在下一篇文章與大家分享,因為這篇又爆字數了 XXD。
好用的工具百百種,但效能優化卻是大家很容易忽略的一點,在使用的同時,也要記得不論是多麽強大的武器,如果不能適當地回收資源與保養,總有一天也是會累垮的。
以上有任何問題,都歡迎留言討論唷。