在單執行緒的 JavaScript 中,所有的任務都在主執行緒上執行,包括渲染、事件處理與計算邏輯。這導致在進行複雜的計算時,可能會引發畫面卡頓或掉幀的情況。為了解決這類效能問題,Web Worker 提供了一種多執行緒處理的方式,能夠將計算密集的任務移出主執行緒,從而提升畫面的流暢度。
在先前文章中,我們探討了單執行緒渲染的效能瓶頸,特別是在處理大量粒子系統的時候,當計算引力和模擬碰撞時,效能表現尤為不佳。這是因為每幀都需要進行大量的計算,這不僅拖慢了渲染速度,還導致了用戶體驗的下降。
Web Worker 是一種允許在 JavaScript 中執行多執行緒任務的 API。透過將 Web Worker 整合到渲染過程中,可以把耗時的運算轉移到背景執行,而主執行緒則負責畫面更新與控制,使整體的畫面渲染更加流暢。
const worker = new Worker('./worker.js');
worker.postMessage(可轉移對象);
worker.onmessage = function (e) {
console.log(e.data);
};
能夠和 Web Worker 傳輸的資料格式是有限的,它必須是可轉移對象Transferable objects,不過還是蠻多種類的,從基本的物件、音訊、點陣圖、到離屏 Canvas 都可以。同時要注意的是,在 Web Worker 的作用域中不包含 window 物件。
因此,我們需要一個介面和它進行溝通,剛好,我們先前就做了一個前端和演算法之間的介面,讓我們只保留和 Web Worker 有關的程式片段:
const createLokaVolterra = function(){
this.setCanvas = (canvas, bitmap) => {
this.canvas = canvas;
this.myWorker = new MyWorker();
this.offscreen = bitmap.transferControlToOffscreen();
this.myWorker.postMessage({
"name": "transferControlToOffscreen",
"canvas": this.offscreen,
}, [this.offscreen]);
}
this.cleanup = () => {}
this.resize = () => {
this.myWorker.postMessage({
"name": "setOffscreen",
"w": this.canvas.width,
"h": this.canvas.height
});
}
this.pauseWorker = (isPause) => {
this.myWorker.postMessage({"name": isPause ? "requestAnimation" : "cancelAnimation"});
}
this.render = () => {}
this.update = () => {}
return this;
}
在背景執行緒中,我們根據不同的任務名稱來分配處理,這些任務包括 Canvas 控制權的轉移、Offscreen 畫布的設定、動畫的暫停和播放。
import lokaVolterraAlgorithm from "./lokaVolterraAlgorithm";
// 控制介面
self.onmessage = function handleMessageFromMain(msg) {
switch(msg.data.name){
case "transferControlToOffscreen":
canvas = msg.data.canvas;
ctx = canvas.getContext('2d');
algorithm.reset(canvas.width, canvas.height);
break;
case "setOffscreen":
canvas.width = msg.data.w;
canvas.height = msg.data.h;
break;
case "cancelAnimation":
cancelAnimationFrame(requestID);
requestID = undefined;
break;
case "requestAnimation":
if(requestID) self.postMessage({"name": "error", "message": "requestAnimation existed"});
else requestID = requestAnimationFrame(main);
}
};
let requestID = undefined;
let canvas;
let ctx;
const algorithm = new lokaVolterraAlgorithm();
// 繪圖系統-main
function main() {
algorithm.render(ctx, 0.25);
algorithm.update(ctx, canvas.width, canvas.height);
requestID = requestAnimationFrame(main);
}
我們的動畫管理員不僅僅是管理動畫的註冊與運行,還能夠註冊特定的觸發按鈕。這些按鈕負責在畫面進入與離開時控制動畫的播放與暫停。
例如,在場景中,當用戶進入一個區域或視口中,觸發對應的動畫,當離開時自動暫停,這樣既節省了計算資源,也增強了使用者體驗:
this.registerTrigger = (name, enter, leave) => {
this.trigger[name] = {enter, leave};
this.nameValidation(name);
}
this.updateRequestAnimation = (id) => {
// 停止舊的動畫
this.lastTriggerName.forEach(name => {
if(!this.trigger[name]) return;
this.trigger[name].leave();
})
const triggerNames = this.getTriggerById(id);
this.lastTriggerName = triggerNames;
// 開始新的動畫
triggerNames.forEach(name => {
if(typeof this.trigger[name] === "undefined") return console.warn("invalid trigger");
this.trigger[name].enter();
})
}
在 React 框架下,我們將這一套動畫管理系統集成到 Canvas 元件中,使其與 React 的生命周期保持一致。透過交互觀測技術(如 IntersectionObserver),我們可以準確地控制動畫的播放時機:
useEffect(()=>{
manager.registerTrigger(
"worker" + sectinoID,
() => lokaVolterra.pauseWorker(true),
() => lokaVolterra.pauseWorker(false)
);
return () => {
manager.unRegisterTrigger("worker" + sectinoID);
}
}, []);
或者,作為一個按鈕提供使用者播放和暫停:
const [isWorker, setIsWorker] = useState(true);
function handlePauseWorker(){
lokaVolterra.pauseWorker(!isWorker);
setIsWorker(!isWorker);
}
<button onClick={handlePauseWorker} id="pauseWorker">
{isWorker ? "停止" : "開始"}
</button>
為了評估多執行緒渲染的效能提升,我們進行了以下測試:
在測試中,故意增加了粒子數量造成負載,我記錄了自己電腦每幀的渲染時間,測試結論和我之前的相關實驗相去不遠,當時是讓 Web Worker 用 bitmap 圖片的形式傳回主執行緒,這次則是用離屏渲染的方法。
結果顯示,Web Worker 搶走了主執行緒大約 20% 的算力,我認為這跟現代處理器的多核心加速有關,這表示其他核心閒置的時候,確實能對主執行緒提供幫助。但是,Web Worker 實際上又提供了約 70% 的算力,因此,只要使用得當,多執行緒確實能提高演算法的上限。
儘管 Web Worker 可以有效提升效能,但其在實作中仍面臨一些挑戰與限制:
Web Worker 作為一種多執行緒的解決方案,確實能在特定情況下提升渲染效能,特別是在處理大量計算密集型任務時。然而,效能的提升並非絕對,資料同步與傳輸的成本有時會帶來瓶頸。因此,在考慮使用 Web Worker 的同時,我們應該思考更多的解決方案,如將計算交由伺服器端處理,或使用 WebGL 利用 GPU 加速,而非完全依賴 CPU 進行計算。