「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
在浏览器中 DOM 的渲染计算是由浏览器内部进行计算和处理的,因此前端开发者不需要关注渲染相关的计算。但是 canvas 与其他元素不同,它提供给 JS 开发者自己控制渲染的能力,使前端应用具有更高的灵活性和可编程性。但是与此相对应的,前端开发者需要使用 JS 程序来进行位置形状颜色的计算和控制,这些计算都是与渲染直接相关,都是发生在用户的主线程中,如果程序过于复杂,canvas 的计算可能会影响应用性能和用户体验。
在浏览器中使用 WebWorker 能够创建独立的线程,这个线程 WebWorker 主要是用来处理计算任务的,在 worker 中不可以访问 DOM,而普通的 canvas 元素是挂载在 DOM 树上的,因此不能在 worker 中操作普通的 canvas。因此,为了能够在 worker 中操作 canvas,浏览器提供了一种与 DOM 解耦的 canvas,即 OffscreenCanvas。
首先来看一个 canvas 渲染的例子:(animation.js 使用的是 google 的示例动画,点此查看源码)
<button id="block">block</button><br />
<canvas id="canvas-main" width="400" height="200"></canvas>
<script src="./animation.js"></script>
<script>
document.querySelector("#block").addEventListener("click", () => {
Animation.fibonacci(40);
});
const animationMain = new Animation(document.querySelector("#canvas-main").getContext("2d"));
animationMain.start();
</script>
点击 block 会触发一个 fibonacci 计算逻辑,我们以此来模拟主线程忙碌场景,运行此例会发现当点击 block 时动画会有一段时间的明显卡顿。
现在我们来使用 WebWorker 来优化程序,为了方便对比新创建一个 canvas-worker 元素:
<canvas id="canvas-worker" width="400" height="200"></canvas>
// worker.js
importScripts("./animation.js");
onmessage = (e) => {
const animationWorker = new Animation(e.data.canvas.getContext("2d"));
animationWorker.start();
};
我们设想的是 worker 线程接收一个 canvas 对象,然后在 worker 中进行绘制操作,于是主线程的代码大概是这样的:
const canvas = document.querySelector("#canvas-worker");
worker.postMessage({ canvas });
熟悉 WebWorker 应该就会知道 postMessage 传递消息默认是复制操作,即使这段代码可以工作, worker 中的 canvas 和主线程中的也不是同一个(实际上 canvas 是不可克隆的对象,这段代码不能工作),我们实际上想要的是把 canvas 转移到 worker 中,因此更接近的是这样:
worker.postMessage({ canvas }, [canvas]);
但是 canvas 对象不可克隆的同时也不可转移,因此这段代码也无法运行,不过我们在可转移对象列表中可以看到 OffscreenCanvas 这个对象,就是本文的主角,它是一个可转移的对象。因此现在的问题就变成了如何把 canvas 转化为 OffscreenCanvas,方法很简单,调用 transferControlToOffscreen 方法即可:
const offscreen = document.querySelector("#canvas-worker").transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
OffscreenCanvas 上提供了和 canvas 一样的渲染 API,因此刚刚的 Animation 中传入 OffscreenCanvas 运行的效果和 canvas 完全相同,现在运行示例程序,会发现两个动画都可以正常运行,当点击 block 时,canvas-main 出现停顿,但是 canvas-worker 不受影响。
在 OffscreenCanvas 对象上还有一个 transferToImageBitmap 方法,它可以把 canvas 内容转化为 ImageBitmap 对象,ImageBitmap 也是一个可转移对象,因此也可以通过转移 ImageBitmap 的方式来使用 OffscreenCanvas,可参考 code:
// main
const canvasWorker = document.querySelector("#canvas-worker").getContext("bitmaprenderer");
worker.onmessage = (e) => {
canvasWorker.transferFromImageBitmap(e.data.imageBitmap);
};
// worker.js
const offScreen = new OffscreenCanvas(400, 200);
const animationWorker = new Animation(offScreen.getContext("2d"));
animationWorker.start();
animationWorker.onFrame = () => {
const imageBitmap = offScreen.transferToImageBitmap();
postMessage({ imageBitmap }, [imageBitmap]);
};
这里主线程在收到 imageBitmap 对象时利用 bitmaprenderer 的 transferFromImageBitmap 来进行渲染,这种方式需要依赖逐帧的消息处理,最终响应渲染的还是主线程,因此在 canvas 本身计算量很大时可以使用,但是对于本例主线程阻塞的问题这种方案无法解决问题。在实际开发中 transferControlToOffscreen 相对而言适用范围更广。
OffscreenCanvas 是一个很经典的对 WebWorker 的应用,在浏览器中,canvas 可以做很多复杂的事情,canvas 还是浏览器执行 webgl 的入口,合理使用 OffscreenCanvas 可以极大程度提升应用性能。