OffscreenCanvas—speed up your canvas operations with a web worker

Tim Dresser

Canvas is a popular way of drawing all kinds of graphics on the screen and an entry point to the world of WebGL. It can be used to draw shapes, images, run animations, or even display and process video content. It is often used to create beautiful user experiences in media-rich web applications and online games.

It is scriptable, which means that the content drawn on canvas can be created programmatically, for example, in JavaScript. This gives canvas great flexibility.

At the same time, in modern websites, script execution is one of the most frequent sources of user responsiveness issues. Because canvas logic and rendering happens on the same thread as user interaction, the (sometimes heavy) computations involved in animations can harm the app's real and perceived performance.

Fortunately, OffscreenCanvas is a response to that threat.

Browser Support

  • 69
  • 79
  • 105
  • 16.4

Source

Previously, canvas drawing capabilities were tied to the <canvas> element, which meant it was directly depending on the DOM. OffscreenCanvas, as the name implies, decouples the DOM and the Canvas API by moving it off-screen.

Thanks to this decoupling, rendering of OffscreenCanvas is fully detached from the DOM and therefore offers some speed improvements over the regular canvas as there is no synchronization between the two.

What is more, though, is that it can be used in a Web Worker, even though there is no DOM available. This enables all kinds of interesting use cases.

Use OffscreenCanvas in a worker

Workers are the web's version of threads—they allow you to run tasks in the background.

Moving some of your scripting to a worker gives your app more headroom to perform user-critical tasks on the main thread. Without OffscreenCanvas, there was no way to use the Canvas API in a worker, as there was no DOM available.

OffscreenCanvas does not depend on the DOM, so it can be used. The following example uses OffscreenCanvas to calculate a gradient color in a worker:

// file: worker.js
function getGradientColor(percent) {
  const canvas = new OffscreenCanvas(100, 1);
  const ctx = canvas.getContext('2d');
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
  gradient.addColorStop(0, 'red');
  gradient.addColorStop(1, 'blue');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, ctx.canvas.width, 1);
  const imgd = ctx.getImageData(0, 0, ctx.canvas.width, 1);
  const colors = imgd.data.slice(percent * 4, percent * 4 + 4);
  return `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`;
}

getGradientColor(40);  // rgba(152, 0, 104, 255 )

Unblock main thread

Moving heavy calculation to a worker lets you free up significant resources on the main thread. Use the transferControlToOffscreen method to mirror the regular canvas to an OffscreenCanvas instance. Operations applied to OffscreenCanvas will be rendered on the source canvas automatically.

const offscreen = document.querySelector('canvas').transferControlToOffscreen();
const worker = new Worker('myworkerurl.js');
worker.postMessage({canvas: offscreen}, [offscreen]);

In the following example, the heavy calculation happens when the color theme is changing—it should take a few milliseconds even on a fast desktop. You can choose to run animations on the main thread or in the worker. In case of the main thread, you cannot interact with the button while the heavy task is running—the thread is blocked. In case of the worker, there is no impact on UI responsiveness.

Demo

It works the other way too: the busy main thread does not influence the animation running on a worker. You can use this feature to avoid visual jank and guarantee a smooth animation despite main thread traffic, as shown in the following demo.

Demo

In case of a regular canvas, the animation stops when the main thread gets artificially overworked, while the worker-based OffscreenCanvas plays smoothly.

Because OffscreenCanvas API is generally compatible with the regular Canvas Element, you can use it as a progressive enhancement, also with some of the leading graphic libraries on the market.

For example, you can feature-detect it and if available, use it with Three.js by specifying the canvas option in the renderer constructor:

const canvasEl = document.querySelector('canvas');
const canvas =
  'OffscreenCanvas' in window
    ? canvasEl.transferControlToOffscreen()
    : canvasEl;
canvas.style = {width: 0, height: 0};
const renderer = new THREE.WebGLRenderer({canvas: canvas});

The one gotcha here is that Three.js expects canvas to have a style.width and style.height property. OffscreenCanvas, as fully detached from DOM, does not have it, so you need to provide it yourself, either by stubbing it out or providing logic that ties these values to the original canvas dimensions.

The following shows how to run a basic Three.js animation in a worker:

Demo

Bear in mind that some of the DOM related APIs are not readily available in a worker, so if you want to use more advanced Three.js features like textures, you might need more workarounds. For some ideas on how to start experimenting with these, take a look at the video from Google I/O 2017.

If you're making heavy use of the graphical capabilities of canvas, OffscreenCanvas can positively influence your app's performance. Making canvas rendering contexts available to workers increases parallelism in web applications and makes better use of multi-core systems.

Additional resources