Our site has WebGL smoke, a canvas full of orbits, infinite marquees, twinkling stars and a grainy background. And it runs smooth at 60 fps. Getting there was a matter of measuring layer by layer instead of guessing. Here's what we learned, chapter and verse, to save you the trip.
The golden rule: transform and opacity, nothing else
The browser paints in layers. Animating transform or opacity only moves layers that are already painted (the GPU handles it, for free). Animating almost anything else (filter, box-shadow, width, background-position over large areas) forces a repaint on every frame. One small element gets a pass; twenty elements don't.
Stars that "breathe" by animating filter: drop-shadow get re-rasterized 60 times per second; with ~25 on screen, noticeable stuttering. Ours breathe with transform: scale() and the glow is static. Identical look, zero cost.
mix-blend-mode and filter: the silent killers
A hero with mix-blend-mode for the noise and filter: blur(90px) for the glows, on top of a canvas that repaints every frame, sinks to 5 fps: the blend forces the whole screen to recompose and the blur re-blurs on every repaint. The recipe: glows with pre-blurred radial-gradient (no filter) and zero blend modes over anything animated. Same look, 60 fps.
The rAF loops nobody sees
A ball of light that follows the cursor with smooth interpolation is pretty, and also a requestAnimationFrame loop running always if nobody stops it, even when the mouse has been still for a minute. Every continuous effect has to answer three questions: does it pause off screen? (IntersectionObserver), does it pause when the tab is hidden? (visibilitychange), does it stop when idle? If any answer is no, it's stealing battery.
- Hero canvas: pauses when it leaves the viewport and when the tab is hidden
- CTA WebGL smoke: same, plus DPR capped at 1.5 and automatic resolution downscaling if it detects slow frames
- Megamenu videos: preload=none and they only play while the panel is open
backdrop-filter: you pay on every scroll
A header with backdrop-filter: blur(16px) re-blurs whatever's behind it on every scroll frame. We dropped to 12px and lightened the background: same glass, smaller bill. If your site "stutters when you scroll", start here.
/* BAD: repaints every frame */
.star { animation: glow 2s infinite; }
@keyframes glow { 50% { filter: drop-shadow(0 0 9px gold); } }
/* GOOD: pure compositor, same effect */
.star { filter: drop-shadow(0 0 5px gold); /* static */ }
@keyframes glow { 50% { transform: scale(1.1); } } How we measured it
No intuition: DevTools → Performance with CPU x4, the Paint flashing layer turned on (if something flashes green during an animation, you're repainting), and real recordings on a modest laptop. The site has to run smooth on your client's computer, not yours.
An animation that drops below 60 fps isn't an effect: it's a bug with glitter on it.
Pablo Huget, Web Developer at Intervolutions