Web Workers
18 Jul 2018JavaScript 通常在作業系統的 Main Thread 執行,但若把程式碼放在 Web Workers 就可另闢戰場-Worker Thread,兩條線互不影響,讓 JavaScript 在背景執行,並且兩線可由訊息溝通-使用 postMessage 發送訊息、onmessage 接收訊息。通常我們會將需要長時間運算且不含 Window 或 DOM Element 操作的程式碼放在 Web Workers,好處是不阻塞 Main Thread 而讓速度變快。
圖片來源:Browser Rendering Optimization - JavaScript
瀏覽器支援度
主流瀏覽器皆支援。
注意,Shared Web Workers 目前只能用於 Chrome 66+、Firefox 60+,且手機幾乎不支援。
圖片來源:Can I use…Shared Web Workers
範例
從範例理解 Web Worker 到底是什麼和怎麼用吧 (๑•̀ㅂ•́)و✧
由於 Worker 是需要起 Server 的,推薦使用 Web Server for Chrome 來執行範例網站。
兩數相乘(Dedicated Web Workers)
這裡簡單使用 Dedicated Web Workers 來做相乘運算,算完後再丟回 Main Thread。
說明如下
- 檢查瀏覽器是否支援 Worker api
- 載入 Worker 所用的檔案
worker.js
- 當欄位值有所變更時,以陣列方式傳送資料給 Worker
- 當 Worker 運算完畢回傳給 Main Thread 時,畫面會更新結果
以下是主程式的部分程式碼。
if (window.Worker) {
var myWorker = new Worker('worker.js');
first.onchange = function () {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
};
second.onchange = function () {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
};
myWorker.onmessage = function (e) {
result.textContent = e.data;
console.log('Message received from worker');
};
}
以下是 Web Workers 的部分程式碼,Web Workers 使用 onmessage 監聽 Main Thread 是否傳送資料,收到資料後即在 Worker Thread 進行運算,算完再用 postMessage 回應給 Main Thread(註一)。
onmessage = function (e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + e.data[0] * e.data[1];
console.log('Posting message back to main script');
postMessage(workerResult);
};
注意
- 在 Web Workers 中的 JavaScript 運行在不同於 Window 的執行緒環境,所以在 Web Workers 中存取全域物件應該要透過 self,如果透過 Window 會發生錯誤。
- 在主執行緒中使用 onmessage 與 postMessage 必須掛在 Worker 物件上,在 Worker 執行緒則不用,這是因為 Worker 執行緒的全域物件即是 Worker 物件。
- 若要在 Web Workers 中引入程式碼,必須要透過
importScripts()
。importScripts()
可以讓 Web Workers 引用相同網域的程式碼腳本與函式庫,並且importScripts()
可接收零到多個輸入資源的 URI。
兩數相乘(Shared Web Workers)
Shared Web Workers 能夠被多個程式腳本存取,即使是跨越不同 window、iframe 或 worker。
這個範例依然是兩數相乘,只是分別演示除了兩數相乘外,還可以計算平方,但皆用同一個 Worker 做運算。
這裡有兩個腳本 square.js 和 multiply.js,它們都共用 worker.js 來做兩數相乘的運算。
使用 multiply.js 的畫面。
使用 square.js 的畫面。
和 Dedicated Workers 做法類似(如上例),除了以下三個部份…
一、用 SharedWorker 建構子來產生 Shared Workers。
以下是主程式的部分程式碼。
if (!!window.SharedWorker) {
var myWorker = new SharedWorker('worker.js');
// ...
}
二、與 Shared Worker 的溝通必須要透過 port 物件(註一),其實 Dedicated Workers 也是如此,只不過一切是在背景自動完成。
以下是主程式的部分程式碼。
squareNumber.onchange = function () {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log('Message posted to worker');
};
以下是 Web Workers 的部分程式碼。
onconnect = function(e) { // 監聽連線建立的 onconnect 事件,從 onconnect 取得 port 物件
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
}
三、腳本可共用 Shared Workers 來做運算。例如,square.js 和 multiply.js 共用 worker.js 來做兩數相乘的運算。
Image Manipulator
這裡有一個 Image Manipulator,由於圖片處理是很花效能的,因此希望將這部分的計算移到 Web Workers。
下圖是 Demo,這個 Image Manipulator 在上傳圖片後,可分別做「Invert」、「Chroma」、「Greyscale」、「Vibrant」和「回到原圖」,前四個功能在處理什麼其實我不太清楚,反正就是對圖做些事情摟 XD
改善方式是將這一段計算放到 worker.js
中,並在 worker.js
引入計算所用到的函式庫,待計算完再丟回 Main Thread,然後顯示結果。
改善前。
function manipulateImage(type) {
var a, b, g, i, imageData, j, length, pixel, r, ref;
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
toggleButtonsAbledness();
// 複雜計算 ... 省略程式碼
toggleButtonsAbledness();
return ctx.putImageData(imageData, 0, 0);
};
改善後,複雜計算移到 Worker Thread,算完後丟回 Main Thread。
以下是主程式的部分程式碼。
var imageWorker = new Worker('scripts/worker.js');
function manipulateImage(type) {
var a, b, g, i, imageData, j, length, pixel, r, ref;
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
toggleButtonsAbledness();
imageWorker.postMessage({'imageData': imageData, 'type': type});
imageWorker.onmessage = function(e) {
toggleButtonsAbledness();
var image = e.data;
if (image) return ctx.putImageData(image, 0, 0);
}
// 略...
};
以下是 Web Workers 的部分程式碼。
importScripts('imageManips-improved.js');
this.onmessage = function(e) {
var imageData = e.data.imageData;
var type = e.data.type;
try {
// 複雜計算 ... 省略程式碼
}
postMessage(imageData);
} catch (e) {
function ManipulationException(message) {
this.name = "ManipulationException";
this.message = message;
};
throw new ManipulationException('Image manipulation error');
postMessage(undefined);
}
}
效能比較
改用 Web Workers 後,效能改進多少呢?看 Invert 所花的時間。
改善前 Invert 所花的時間
- manipulateImage 執行時間總共 21.84 ms。
改善後 Invert 所花的時間
- manipulateImage 執行時間總共 2.55ms
- Worker 的執行時間總共 16.75ms。
不阻塞 Main Thread 且變快了 d(`・∀・)b
More
若想再玩玩可以看這個範例-qrcode,分支 solution 即是解答。
關於 Worker 的說明。
備註
- 註一:開啟 port 連線可有以下方式 (1) 在 onmessage 事件下背景完成,或 (2) 主動呼叫
start()
後開始傳送訊息。範例 multiply.js 與 worker.js 因註冊了 onmessage 事件,所以可省略呼叫start()
;然而,若 message 事件是經由addEventListener()
註冊,就需要呼叫start()
了。當使用start()
開啟 port 連線,雙向溝通便需要主執行緒和 worker 兩端都呼叫start()
才能運作。