發揮 JavaScript 多執行緒威力 - Web Worker
6 | 9,999 |
前陣子討論到 Chrome 88+ 加入的 Intensive Throttling 會造成 setInterval 大走鐘,同事提到可用 Worker 避開問題,發文後讀者 Scott 也留言提到。不過,在這個議題上,我覺得搬進 Worker 規避 Chrome 節能設計雖然省事但非最佳解,若有其他更有效率的替代方案,改用更省 CPU 省電的寫法是上策。
但這倒提醒我,自己從沒用過 Web Worker,不清楚其優勢及應用時機,趁此機會補上吧。
Web Worker 已經推出多年,不是什麼新技術,連 IE10 都支援即可見一斑,網路上教學與討論不少,我主要參考阮一峰老師的這篇 - Web Worker 使用教程,寫得很淺白完整,這篇文章會跳過 Web Worker 基本教學,直接來看 Web Worker 的優點及應用場合。
首先,要先確立一個觀念,網頁裡的 JavaScript 一直是用同一條執行緒執行,即便 setTimeout/setInterval 設定完就能跑其他程式,時間到會自動執行指定的函式,給人多執行緒並行的錯覺。事實上,setTimeout/setInterval 執行時會中斷原本執行中的 JavaScript 指令,等其結束後再繼續 setTimeout/setInterval 跟網頁事件與其他 setTimeout/setInterval JavaScript 會排隊執行,因此若 setTimeout/setInterval 跑很久,其他 JavaScript 指令必須等待其結束才能執行,因此便可能出現網頁久無回應被卡住的感覺。Web Worker 的好處即在於它在獨立的背景執行緒執行,不會跟網頁裡的 JavaScript 搶 CPU,這便是我想藉由實驗驗證的點。
要突顯 Web Worker 多執行緒的優勢,要找個網頁 JavaScript 很忙的範例,我找到很吃 CPU 的粒子移動模擬 JavaScript Canvas 效能測試,
至於同時要跑的重量級運算,我很沒創意地選擇計算 0 ~ n 包含的質數,第一個版本用 setTimeout 來做:
<!DOCTYPE html>
<html>
<head>
<title>Benchmark - setTimeout</title>
<script src="excanvas.compiled.js"></script>
<script src="animation.js"></script>
<style>
#info {
position: absolute;
z-index: 99;
color:yellow;
font-size: 14pt;
top: 10px;
left: 10px;
padding: 6px;
background-color: gray;
}
</style>
</head>
<body>
<div id='benchmark' style='width: 800px; height: 400px'></div>
<div id=info>
</div>
<script>
let info = document.getElementById('info');
function isPrime(num) {
if (num <= 3) return num > 1;
if ((num % 2 === 0) || (num % 3 === 0)) return false;
let count = 5;
while (Math.pow(count, 2) <= num) {
if (num % count === 0 || num % (count + 2) === 0) return false;
count += 6;
}
return true;
}
function listPrimes(maxNum) {
let res = [];
for (let i = 1; i <= maxNum; i++) {
if (isPrime(i)) res.push(i);
}
return res;
}
var st = new Date().getTime();
setTimeout(function () {
let res = listPrimes(10000000);
dura = (new Date().getTime() - st);
document.getElementById('info').innerText += dura + 'ms ' + res.length + ' primes';
}, 1000);
</script>
</body>
</html>
計算質數部分我刻意延遲一秒開始,用意讓粒子動晝先播放一陣子,一秒後透過 setTimeout 執行尋找 1 ~ 1000 萬所含質數,如包含一秒延遲,共耗時 3.3s。
接著來改寫 Web Worker 版,寫一支 worker.js:
function isPrime(num) {
if (num <= 3) return num > 1;
if ((num % 2 === 0) || (num % 3 === 0)) return false;
let count = 5;
while (Math.pow(count, 2) <= num) {
if (num % count === 0 || num % (count + 2) === 0) return false;
count += 6;
}
return true;
}
function listPrimes(maxNum) {
let res = [];
for (let i = 1; i <= maxNum; i++) {
if (isPrime(i)) res.push(i);
}
return res;
}
self.addEventListener('message', function (e) {
var res = listPrimes(e.data);
console.log('done');
self.postMessage(res);
self.close();
}, false);
網頁程式部分一樣先跑動晝,setTimeout 延遲一秒啟動 Worker 尋找 1 ~ 1000 萬所含質數:
<!DOCTYPE html>
<html>
<head>
<title>Benchmark - Worker</title>
<script src="excanvas.compiled.js"></script>
<script src="animation.js"></script>
<style>
#info {
position: absolute;
z-index: 99;
color:yellow;
font-size: 14pt;
top: 10px;
left: 10px;
padding: 6px;
background-color: gray;
}
</style>
</head>
<body>
<div class="snowflakes">
</div>
<div id=info>
</div>
<script>
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
let dura = (new Date().getTime() - st);
document.getElementById('info').innerText = dura + 'ms ' + e.data.length + ' primes';
console.log(new Date());
}
var st = new Date().getTime();
setTimeout(() => {
worker.postMessage(10000000);
}, 1000);
</script>
</body>
</html>
Web Wroker 版,包含 1 秒延遲啟動耗時約 3.5s,咦?怎麼比 setTimout 還慢?建立 Worker 物件及 postMessage 溝通會耗用一些額外資源,但用 Worker 不就為了效能,反而更慢像話嗎?
其實上面我賣了個關子,如果實際看過兩者的執行結果,大家就會知道為什麼該用 Web Worker 了。
左邊的 setTimeout 版本,質數計算期間動晝完全凍結,算完才繼續,且因執行間隔錯亂,粒子原本應隨機亂跑,一度出現整群同步移動。而 Web Wroker 版,全程動畫順暢未受干擾,證明質數計算是用另一個執行緒在跑,不中斷網頁的 JavaScript 執行,真正實踐了多執行緒。
最後再補充另一個實驗,瀏覽器本身是多執行緒環境,受單一執行緒限制的是網頁的 JavaScript 程式,其他如 Render、CSS 等運算等作業,瀏覽器會安排不同執行緒處理。因此,如果今天是用純 CSS 製作的動晝(我找到一個雪花飄效果當範例),用 setTimeout 或 Web Worker 的差異不大。(註:測量結果未包含一秒延遲,故比之前少一秒)
從以上實驗,我的結論是 - 對於等待外部回應或短短幾行的簡單作業,用 Web Worker 有點殺雞用牛刀,寫成 setTimeout/setInterval 就夠了,程式還比較簡潔;但如果是會持續數秒以上的重度運算,若不想在計算過程畫面卡死凍結,便可借重 Web Worker 發揮多執行緒威力,提供更好的使用者體驗。
想動手玩看看的同學,線上版本在這裡:
- JavaScript 粒子模擬: setTimeout 版、Web Worker 版
- 純 CSS 雪花動畫: setTimeout 版、Web Worker 版
A example of when to use web worker to improve user experience.
Comments
# by 布丁布丁吃布丁
不錯的實驗
# by 阿尼
讚讚
# by Dennis
>> setTimeout/setInterval 執行時會中斷原本執行中的 JavaScript 指令,等其結束後再繼續。 Wrong
# by Karcher
web worker讓js不在受限於單執行緒 學習到了,謝謝
# by CY
>># 2021-10-05 06:39 PM by Dennis >>>> setTimeout/setInterval 執行時會中斷原本執行中的 JavaScript 指令,等其結束後再繼續。 >>Wrong 如同 Dennis 所抓出的這句,其實這樣講不太對,甚至應該是相反。 setTimeout/setInterval 只能保證 callback 不「早於」所預定的時間執行,但會晚於預期的時間, 所以如果有原本執行中的 「同步」型 JavaScript 指令,會等到全部執行完, 才會輪到 event loop 裡的其他 callback 執行,如已到期的 Timer 、async 操作有結果要被傳入執行的 callback、事件發生要被 trigger 的 listener 等等。
# by Jeffrey
to Dennis, CY,謝謝指正。 我原先把網頁的事件、其他 setInterval/setTimeout 想像成一整包不再細分的程式,進而衍生原本該跑的程式被它卡住延後執行,原想藉此忽略細節較易理解,但同意兩位的觀點,此一說法讓讀者對 JavaScript Event Loop 機制產生誤解,已修改原文。