All guides

Animated Backgrounds in React Without Hurting Performance

By Hizmedia · · 6 min read

An animated background is often the first thing on a page and the last thing that should slow it down. Done carefully, an aurora, a star field or a flow of particles costs almost nothing. Done carelessly, it drains batteries and makes scrolling stutter. These are the techniques MotionKit's backgrounds use.

Choose the lightest technique that works

  • CSS for soft colour: blurred gradients that drift or rotate. One transform animation on a large blurred layer is very cheap; animating filter: blur() itself every frame is not.
  • SVG for lines and shapes that morph: contour lines, drawn paths, gooey blobs with a filter.
  • Canvas for many moving things: hundreds of particles, a grid of thousands of squares, a 3D projection. One canvas element is far cheaper than thousands of DOM elements.

Stop when nobody can see it

A background that keeps animating after it's scrolled away wastes battery for nothing. Observe it and only run the loop while it's on screen.

loop.ts
let frame = 0;
let visible = true;
const observer = new IntersectionObserver(([entry]) => {
  visible = entry.isIntersecting;
  if (visible && !frame) frame = requestAnimationFrame(loop);
});
observer.observe(canvas);

function loop(time: number) {
  draw(time);
  frame = visible ? requestAnimationFrame(loop) : 0;
}

Browsers already pause requestAnimationFrame in background tabs, so this covers the case they don't: a tab that's open but scrolled past the hero.

Draw sharp on high-density screens

A canvas sized to its CSS box looks blurry on retina screens. Size the drawing buffer by devicePixelRatio, then scale the context so you can keep drawing in CSS pixels. Measure on resize with a ResizeObserver, not on every frame: reading layout in an animation loop forces the browser to recalculate it each time.

resize.ts
const resize = () => {
  const { width, height } = canvas.getBoundingClientRect();
  const ratio = window.devicePixelRatio || 1;
  canvas.width = width * ratio;
  canvas.height = height * ratio;
  context.setTransform(ratio, 0, 0, ratio, 0, 0);
};
new ResizeObserver(resize).observe(canvas);

Respect reduced motion and keep text readable

With prefers-reduced-motion: reduce, draw a single still frame instead of starting the loop: the background keeps its look without moving. Then check contrast at the brightest moment, not the average: a vignette or a dark centre behind your headline keeps text readable while colour moves round the edges.

Keep it out of the critical path

The background should never delay your headline. Render the hero's text as plain HTML so it paints immediately, give the background its final size from the start so nothing shifts, and let the animation begin after the page is interactive.