Canvas 离屏渲染

Canvas

概述

离屏渲染是一种将渲染操作从显示设备分离的技术。在 Web 开发中,主要有两种实现方式:

  1. 传统离屏渲染:使用隐藏的 Canvas 在主线程内存中完成渲染
  2. 现代离屏渲染:使用 OffscreenCanvas API 在 Worker 线程中进行渲染

传统离屏渲染

传统离屏渲染通过在内存中创建缓冲区完成渲染操作,然后将结果复制到显示设备。 实现如下:

// 1. 创建离屏缓冲区
const buffer = document.createElement('canvas')
const ctx = buffer.getContext('2d')

// 2. 在缓冲区中渲染
ctx.drawImage(sourceImage, 0, 0)
ctx.filter = 'blur(5px)'

// 3. 将结果复制到显示设备
displayContext.drawImage(buffer, 0, 0)

这种方式的优点是支持批量渲染、可以缓存结果等,缺点是占用主线程资源、额外的内存开销、不适合复杂动画。适用于以下场景:

  1. 图像处理:滤镜应用、图像合成、像素操作
  2. 性能优化:缓存静态内容、批量渲染操作、避免重复计算

现代离屏渲染:OffscreenCanvas

OffscreenCanvas 是现代 Web 平台提供的新一代离屏渲染 API,它允许将 Canvas 渲染操作从主线程转移到 Web Worker 中进行。

基本用法如下:

第一步:主线程转移 Canvas 控制权到 Worker 线程

 // 创建 Worker
 const worker = new Worker(new URL('./render.worker.js', import.meta.url), {
   type: 'module',
 });

 // 转移 canvas 控制权
 const offscreen = canvas.transferControlToOffscreen();
 worker.postMessage({ canvas: offscreen }, [offscreen]);

第二步:Worker 线程执行渲染

self.onmessage = (evt) => {
  const canvas = evt.data.canvas
  const ctx = canvas.getContext('2d')

  function render() {
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    // 绘制内容
    ctx.fillStyle = 'green'
    ctx.fillRect(100, 100, 200, 200)
    // 继续下一帧
    requestAnimationFrame(render)
  }

  render()
}

这两步涉及到以下技术原理:

  • 首先是使用 Transferable 对象机制将 Canvas 控制权转移给 Worker;转移后主线程的 Canvas 元素仍然可见,但无法直接操作;并且转移是单向的,一旦转移无法收回控制权
  • 其次是主线程的 Canvas 元素和 Worker 线程的 OffscreenCanvas 共享同一个底层位图缓冲区,渲染命令直接更新共享缓冲区,无需线程间的数据传输;当浏览器检测到缓冲区更新后,会自动将缓冲区内容与其他 DOM 元素合成,最终合成结果显示在屏幕上,即使主线程被阻塞也不会影响渲染结果
  • 线程间通信:渲染数据通过共享的位图缓冲区自动同步,无需手动传递;控制信息使用 postMessage 在线程间传递(如尺寸变化、动画控制等)

下面通过一个简单的 Demo 来对比展示普通画布与 OffscreenCanvas 的渲染效果:

可以看到:当我在控制台执行一段阻塞主线程的代码时,普通画布的渲染会卡顿甚至停止,而 OffscreenCanvas 的渲染则不受影响。

由于采用共享缓冲区机制,整个渲染过程无需在线程间复制像素数据,从而保证了高性能的画面更新。这种机制也使得 OffscreenCanvas 在主线程执行复杂计算时仍能保持画面的流畅更新,有效解决了传统 Canvas 在主线程阻塞时渲染卡顿的问题。

因此,当遇到以下场景时,可以考虑使用 OffscreenCanvas

  • 需要处理复杂的 2D 渲染(如图表、图像处理)
  • 需要执行大量动画计算(如粒子效果、物理模拟)
  • 需要同时处理多个渲染任务
  • 渲染操作影响了页面的交互响应

总结对比

传统离屏渲染虽然支持批量渲染和缓存结果等优点,但也存在占用主线程资源、产生额外内存开销以及不适合复杂动画等局限性。相比之下,OffscreenCanvas 结合 Worker 的方案能够实现渲染不阻塞主线程,特别适合复杂的动画渲染,有效提升页面响应性能。然而,这种方案也面临浏览器兼容性限制、无法在主线程直接操作画布以及存在线程间通信开销等问题。