Worker中的OffscreenCanvas渲染實踐與淺析

Tl;DR:html

  1. OffscreenCanvas可讓你在Worker線程中渲染圖形,支持多種RenderingContext
  2. 兩種使用方式:同步的Transfer模式與異步的Control模式
  3. 將Canvas的邏輯計算與渲染分離,避免UI線程阻塞

介紹

產生的契機:用戶在交互時的Canvas邏輯與渲染在同一線程內執行,動畫產生的卡頓可能會影響用戶體驗。若在後臺渲染,則能夠避免耗時的渲染任務阻塞主線程。webpack

使用OffscreenCanvas與Worker結合的方式能夠將渲染任務放在子線程中,有效提高用戶交互時的界面流暢度。git

兩種使用方式

Transfer模式Control模式github

本身起的名字,參考了這篇文章。web

Transfer模式

  1. worker線程中建立OffscreenCanvas對象並執行渲染,給主線程返回結果(緩衝區圖像或其它數據)
  2. 主線程使用緩衝區數據渲染Canvas元素

worker線程算法

let offscreen = new OffscreenCanvas(w,h);
let ctx = offscreen.getContext('2d');
// 一些渲染操做...
let image = offscreen.transferToImageBitmap();
self.postMessage({ image }, [image]);
複製代碼

主線程chrome

renderWorker.onmessage = msg => {
  let imageBuffer = msg.data.image;
  let bitmapContext = canvas.getContext("bitmaprenderer");
  bitmapContext.transferFromImageBitmap(imageBuffer);
}
複製代碼

這種方式能夠用於H5遊戲的精靈加載,文本渲染、生成海報等固定的渲染任務。canvas

Control模式

  1. 主線程中移交Canvas元素的控制權
  2. 在worker線程執行全部的渲染操做,無需圖像數據的傳遞便可更新Canvas元素

在該模式下不須要transfer的相關操做,內部直接對綁定的dom元素進行更新。dom

主線程異步

const offscreen = document.querySelector('canvas').transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
複製代碼

worker線程

onmessage = e => {
  let canvas = e.data.canvas
  let ctx = canvas.getContext('2d');
  // 一些渲染操做...
  // 這裏的渲染操做會在Canvas元素上同步繪製的圖像
}
複製代碼

加載worker文件

在使用webpack進行構建的項目中,對於worker文件須要進行一些額外的處理。

目前生態中主要有三種處理web worker的loader: worker-loader,workerize-loadercomlink-loader

關於這幾種loader的介紹能夠看看這篇後面的loader部分。

經測試,因爲workerize-loader目前沒有對postMessage方法中的Transferable參數序列進行處理,所以沒法將主線程的OffscreenCanvas對象傳入worker中,詳情見這個issueworker-loader表現正常

圖片處理

在worker中使用圖像數據與主線程中有所不一樣。

主線程

const loadImage = imgPath => {
  return new Promise((resolve, reject) => {
    let img = new Image();
    img.setAttribute("crossOrigin", "anonymous"); // to solve "Tainted canvases may not be exported" error
    img.onload = () => { resolve(img); };
    img.onerror = e => { reject(new Error(e));};
    img.src = imgPath;
  });
};

const image = await loadImage(url)
// use image...
複製代碼

worker線程

const response = await fetch(url);
const blob = await response.blob();
const image = await createImageBitmap(blob);
// use image...
複製代碼

動畫驅動

在worker線程中執行動畫有兩種方式:

  1. Timer - setTimeout, setInterval等
  2. rAF - rAF在DedicatedWorker中已經實現,與Window中的行爲一致。

實踐

瞭解了OffscreenCanvas與worker的相關特性,不如動手嘗試一下,以一個蒙版合成的渲染任務爲例。

在主線程中執行繪製

/* 0. 獲取Canvas元素 */
let mainCanvas = document.querySelector('#canvas');
let mainCtx = mainCanvas.getContext('2d');
/* 1. 準備Image對象 */
let img = await loadImage(imgPath);  
...
/* 2. 建立一個Canvas來合成結果圖像 */
let maskLayer = document.createElement("canvas");
maskLayer.width = width;
maskLayer.height = height;
const maskCtx = maskLayer.getContext("2d");
maskCtx.drawImage(img, 0, 0);
let maskData = maskCtx.getImageData(0, 0, width, height);
for (let i = 0; i < width * height; i++) {
  if (values[i] !== 255) {
    maskData.data[(i + 1) * 4 - 1] = mask[i];
  }
}
maskCtx.putImageData(maskData, 0, 0);
...
/* 3. 繪製到Canvas元素上 */
mainCtx.drawImage(maskLayer, 0, 0);
複製代碼

在worker線程中執行繪製

主線程

import CanvasWorker from "worker-loader!@/workers/canvas.worker.js";
...
/* 0. 獲取Canvas元素 */
let canvas = document.querySelector('#canvas');
let offscreenCanvas = canvas.transferControlToOffscreen();
let canvasWorker = new CanvasWorker();
// 將綁定的offscreenCanvas實例傳遞到worker線程中
canvasWorker.postMessage({ canvas: offscreenCanvas, event: "init" }, [offscreenCanvas]);
...
/* 2.發送繪製事件 */
let img = this.resultImg || this.img;
canvasWorker.postMessage({
  event: "draw"
  payload: JSON.stringify({ width, height, imgSrc, mask }) // 因爲結構化克隆算法的限制,這裏對參數對象進行JSON序列化後賦值
});
複製代碼

worker線程

let canvas, ctx;
onmessage = async e => {
  const data = e.data;
  const { payload, event } = data;
  switch (event) {
    case "init": {
      // 保存傳入的OffscreenCanvas實例
      canvas = data.canvas;
      ctx = canvas.getContext("2d");
      break;
    }
    case "draw": {
      /* 0. 解析參數 */
      let { width, height, mask } = JSON.parse(payload);
      ...
      /* 1. 下載圖片並獲取的ImageBitmap數據 */
      const response = await fetch(imgSrc);
      const blob = await response.blob();
      const imageBitmap = await createImageBitmap(blob);
      ...
      /* 2. 建立一個新的OffscreenCanvas來合成結果圖像 */
      let maskLayer = new OffscreenCanvas(width, height);
      const maskCtx = maskLayer.getContext("2d");
      maskCtx.drawImage(imageBitmap, 0, 0); // 使用ImageBitMap繪製圖片
      let maskData = maskCtx.getImageData(0, 0, width, height);
      for (let i = 0; i < width * height; i++) {
        if (values[i] !== 255) {
          maskData.data[(i + 1) * 4 - 1] = mask[i];
        }
      }
      maskCtx.putImageData(maskData, 0, 0);
      ...
      /* 3. Canvas元素上會更新繪製效果 */
      ctx.drawImage(maskLayer, 0, 0);
      break;
    }
  }
};
複製代碼

淺析

極簡Chromium渲染流水線:

blink ---(Main Frame)---> Layer Compositor ---(Compositor Frame)---> Display Compositor ---(GL/UI Frame)---> Window

  • 在OffscreenCanvas中渲染省去了Main Frame中的部分計算任務。
  • Control模式中OffscreenCanvas對Canvas元素的更新再也不與主線程中的其餘元素保持同步,由於它們經過不一樣的渲染流水線。
  • OffscreenCanvas類中提供了一些Layer Compositor階段的執行方法:Commit, BeginFramePushFrame等。處理後的數據直接交給Display Compositor渲染,走最短的渲染路徑。
  • 使用Transfer模式能夠實現Canvas元素與其它元素的同步更新。

blink中offscreen_canvas與html_canvas_element的主類源碼:

OffscreenCanvasHTMLCanvasElement共同繼承的類:

  • ImageBitmapSource - ImageBitmapSource API
  • CanvasRenderingContextHost - 文檔中的描述:the base class for all elements that can host a rendering context,包含通用的數據轉換、尺寸設置、屬性獲取等方法

兼容性

其餘

參考

相關文章
相關標籤/搜索