Improved Performance with OffscreenCanvas

When doing advanced animations or illustrations on the web, many developers opt to use canvas. It is extremely flexible and a great choice for many projects. However, one serious drawback is that it renders on the same thread as the rest of the UI. So, if you have intensive canvas calculations, then scrolling, typing and other interactions with your page can become janky and slow.

With the introduction of OffscreenCanvas, it is now possible to render a canvas using a web worker, meaning that all the heavy lifting is done on a separate thread.

Why is it needed?

Browsers are single-threaded in their execution. This means that running your Javascript, parsing your CSS, drawing your canvasses and rendering your site happen one after the other, not in parallel. If any one of these takes a long time, all of the other parts must wait until it has finished. If it takes five seconds to run your Javascript or draw your canvas, then the user can’t click or scroll or do anything on your site for those five seconds.

Web workers, introduced a few years ago, help to solve this problem. They allow you to run a separate Javascript file in a separate thread. The main limitation is that they do not have access to the DOM, because of the issues that would arise from asynchronous changes to the DOM. Up until now, web workers have mainly been useful for running complex calculations or background tasks that did not need to directly interact with the page.

Drawing to the canvas is often a bottleneck on websites, as the operations can get complex quickly. This can delay the rest of the browser render process, as it waits for the canvas to finish its work. OffscreenCanvas fixes this problem by allowing you to draw a canvas in a web worker, and automatically render it in the DOM. In this scenario, the canvas gets a whole thread to itself. This means that no matter how complex the canvas, it will not slow down the rest of the page, because the browser is not waiting for it. There are huge potential performance improvements to be gained from OffscreenCanvas.

Browser Support

As of writing, OffscreenCanvas is only supported in Chrome (since version 69). Firefox partially supports it behind a config flag (gfx.offscreencanvas.enabled), though it doesn’t appear a full release will be happening soon. Neither Edge nor Safari have given any indication that Offscreen Canvas is on their roadmap.

As always, more info can be found at CanIUse.

How do I use it?

There are two ways to create an OffscreenCanvas. The easiest is to take an existing canvas from the DOM, and generate an OffscreenCanvas from it. The other way is to create a separate OffscreenCanvas, and then manually bind its rendering to a DOM canvas.

transferControlToOffscreen

The arrival of OffscreenCanvas introduces a new function on the canvas, transferControlToOffscreen(). It returns an OffscreenCanvas, which can then be transferred to your web worker. Operations on this OffscreenCanvas will be drawn automatially to the main canvas. Be aware that if you have already used getContext on this canvas, you will not be able to transfer it. In that case, you will either need to replace it with a new canvas, or create a separate OffscreenCanvas, as described in section after this.

const canvas = document.querySelector('#canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();

const worker = new Worker('canvasworker.js');
worker.postMessage({msg: 'init', canvas: offscreenCanvas}, [offscreenCanvas]);

The introduction of OffscreenCanvas also brings support for requestAnimationFrame within web workers. It’s important to realise that this frame is only within the context of the worker, so it is not affected by slow operations on the main thread.

// canvasworker.js

let canvas;
let ctx;

function animate() {
  // do some animations ...
  self.requestAnimationFrame(animate);
}

self.onmessage = function(ev) {
  if(ev.data.msg === 'init') {
    canvas = ev.data.canvas;
    ctx = canvas.getContext('2d');
    animate();
  }
}

new OffscreenCanvas

You may want to create the offscreen canvas separately, as it gives you more flexibility. It allows you to draw on the main canvas from other sources, whereas in the previous example, it becomes tied to the OffscreenCanvas. Additionally, this way would allow you to draw to multiple canvasses efficiently. However, for most cases, transferControlToOffscreen is probably the best option.

The code here is a bit more complex because we have to manually render the OffscreenCanvas into the on-screen one. We do this by emitting a render event (you can call it anything) from the worker when the OffscreenCanvas is ready. Included in this event is an ImageBitmap, which can be drawn very quickly to our main canvas.

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('bitmaprenderer');
const offscreenCanvas = new OffscreenCanvas(canvas.width, canvas.height);

const worker = new Worker('canvasworker.js');
worker.postMessage({msg: 'init', canvas: offscreenCanvas}, [offscreenCanvas]);

worker.addEventListener('message', function(ev) {
  if(ev.data.msg === 'render') {
    ctx.transferFromImageBitmap(ev.data.bitmap);
  }
});

The worker is similar to the previous example, except that it must notify the main thread when the offscreen canvas is ready.

// canvasworker.js

let canvas;
let ctx;

function animate() {
  // do some animations ...

  const bitmap = canvas.transferToImageBitmap();
  self.postMessage({msg: 'render', bitmap});
  self.requestAnimationFrame(animate);
}

self.onmessage = function(ev) {
  if(ev.data.msg === 'init') {
    canvas = ev.data.canvas;
    ctx = canvas.getContext('2d');
    animate();
  }
}

Demo

The demo below demonstrates not just how to use Offscreen Canvas, but also the difference it makes to the user experience when the heavy rendering is done on another thread. Try comparing the difference in responsiveness between the slowed animation on the main thread, and when it is in the web worker.

For simplicity, the demo uses transferControlToOffscreen, but it could easily be modified to use the OffscreenCanvas constructor instead.

Note also that the web worker is not actually a separate file like it normally would be. Instead, the script is converted to an object URL and the worker is created that way. This allows the same script to be used both on the main thread, and in a worker, showing the performance difference in a simple way.

Ready to use?

It is very early days for OffscreenCanvas right now. Chrome just added support in version 69, released this month. Firefox, Edge and Safari do not have a release on the cards any time soon. So, for now this definitely fits in the category of progressive enhancement.

As the demo above shows, it is possible to use the same code both on the main thread and in a web worker. If you want to improve your website’s performance, you could transfer the canvas off screen if supported, and run it on the main thread otherwise. This might not be practical for all cases, but it is certainly worth investigating, because the gains can be significant.

Other recent posts: