DEV Community

Cover image for Don't use rAF with WebGPU (and canvas in general)
Alexander Drozdov
Alexander Drozdov

Posted on

Don't use rAF with WebGPU (and canvas in general)

So you want a pixel perfect rendering. You do a little bit of research and discover the ResizeObserver (RO) API with device-pixel-content-box. This enables setting of an integer number of screen pixels for images in canvas "swapchain".

What you also want is to run animations with a speed of screen refresh rate, using a familiar requestAnimationFrame(rAF).

Challenges arise in combining both APIs, each case presenting a downside:

  • Rendering in rAF while resizing in RO results in an undesirable black frame effect.
  • Rendering in rAF, resizing in RO, and rendering again resolves the issue but at the cost of rendering it twice!
  • Attempting to optimize by saving size in RO for the next rAF introduces a one-frame delay on resize. The canvas image appears stretched during resizing.

As we can see, layout phase in browsers happens after the rAF, therefore we can't know in advance that size will change and suspend the rendering in order to do it in the ResizeObserver callback instead.

Now, consider to stop using rAF for animations completely.
ResizeObserver has all the properties we want from rAF:

  • Operates at the screen refresh rate.
  • Pauses execution when the browser tab is invisible.
  • Allows queuing for subsequent runs within its callback, even when no resizing is occurring.

So instead of conventional loop

function rafLoop() {
  device.queue.submit([cmdBuffer.finish()]);
  requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
Enter fullscreen mode Exit fullscreen mode

Do a ResizeObserver based loop

const observer = new ResizeObserver(roLoop);

function roLoop(resizeEntries: ResizeObserverEntry[]) {
  if (canvasEl.size !== resizeEntries[0]) {
    canvasEl.size = resizeEntries[0];
    // resize depth texture, etc
  }

  device.queue.submit([cmdBuffer.finish()]);

  // unobserve to trigger the callback
  observer.unobserve(canvasEl);
  observer.observe(canvasEl, { box: "device-pixel-content-box" });
}
observer.observe(canvasEl, { box: "device-pixel-content-box" });
Enter fullscreen mode Exit fullscreen mode

That's it. Now we have a perfect rendering during resizing.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

Cloudinary image

Optimize, customize, deliver, manage and analyze your images.

Remove background in all your web images at the same time, use outpainting to expand images with matching content, remove objects via open-set object detection and fill, recolor, crop, resize... Discover these and hundreds more ways to manage your web images and videos on a scale.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay