昨天的範例中顯示當圖片的解析度很高時,執行 getImageData 會很久的問題 (我的電腦 Chrome 瀏覽器耗時 200~300ms 左右),但如果想將這段邏輯移到 Web worker 中處理,又會受限於 worker 無法直接對 DOM 元素進行操作的問題。
為了改善這種狀況,OffscreenCanvas 出現了,OffscreenCanvas 將常見用來操作 canvas 的 api (getImageData, putImageData 等) 與 DOM 元素解耦,讓我們可以在 worker 中直接對 canvas 進行操作。
OffscreenCanvas 的使用主要分為兩種:
這個方式會利用 OffscreenCanvas.transferToImageBitmap 將 OffscreenCanvas 轉換為 ImageBitmap,再將此 ImageBitmap 丟入 ImageBitmapRenderingContext.transferFromImageBitmap 渲染到另一個 canvas 中
const offscreen = new OffscreenCanvas(256, 256);
const context = offscreen.getContext('2d');
// 對 OffscreenCanvas 進行一些圖像操作
context.fillStyle = '#FF0000';
context.fillRect(0, 0, offscreen.width, offscreen.height);
// 將 OffscreenCanvas 轉換為 ImageBitmap
const bitmap = offscreen.transferToImageBitmap();
// 最終想要渲染圖像上去的 canvas
const targetCanvas = document.getElementById('canvas');
const targetContext = targetCanvas.getContext('bitmaprenderer');
// 傳入 OffscreenCanvas 的 ImageBitmap,最終渲染在 targetCanvas 上
targetContext.transferFromImageBitmap(bitmap);
以下範例由主線程發出訊號告知 worker 創建 OffscreenCanvas,並在 worker 中繪製圖像,繪製完後再將 bitmap 傳回主線程的 canvas 並渲染在畫面上
// 主線程
const worker = new Worker('workers.js');
document.querySelector('button').addEventListener('click', () => {
worker.postMessage('create-canvas');
})
worker 線程接收到訊號後開始繪圖,繪圖完後利用 transferToImageBitmap 將 bitmap 轉移回主線程
// worker 線程
self.onmessage = () => {
const offscreen = new OffscreenCanvas(256, 256);
const context = offscreen.getContext("2d");
context.fillStyle = "#FF0000";
context.fillRect(0, 0, offscreen.width, offscreen.height);
const bitmap = offscreen.transferToImageBitmap();
self.postMessage({ bitmap }, [bitmap]);
};
主線程接收到 ImageBitmap 後,可以利用 transferFromImageBitmap 將 ImageBitmap 渲染到指定的 canvas 上
// 主線程
const worker = new Worker('workers.js');
worker.addEventListener('message', (e) => {
const { bitmap } = e.data;
const canvas = document.getElementById('canvas');
const context = canvas.getContext('bitmaprenderer');
// bitmap 的圖像資訊將會被渲染到 canvas 上
context.transferFromImageBitmap(bitmap);
});
第二個方式則是利用 canvas.transferControlToOffscreen,將原本的 canvas 轉換為 OffscreenCanvas,然後這個 OffscreenCanvas 就可以被轉移到 worker 中繪圖操作,這個方法相較於第一個方式較為直覺,也比較常用
const canvas = document.getElementById("canvas");
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker("worker.js");
worker.postMessage({ canvas: offscreen }, [offscreen]);
以下範例調用 canvas.transferControlToOffscreen 函數在 Web worker 中操作 canvas
首先主線程調用 canvas.transferControlToOffscreen 後,可以將 OffscreenCanvas 的所有權轉移到 worker 線程
// 主線程
const worker = new Worker('workers.js');
document.querySelector('button').addEventListener('click', () => {
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
})
接著 worker 線程就可以直接用一般 canvas 常見的方法操作圖像,而這裡的操作也會直接反映在原本主線程的 canvas 上
// worker 線程
self.onmessage = (e) => {
const { canvas } = e.data;
const context = canvas.getContext("2d");
context.fillStyle = "#FF0000";
context.fillRect(0, 0, canvas.width, canvas.height);
};
利用 OffscreenCanvas 可以解耦 canvas 操作圖像與 DOM 之間的關係,因此可以在 worker 線程中操作圖像,改善了原本在主線程中進行耗時的 canvas 操作造成 UI 渲染阻塞問題。