Web Animation Performance — The Complete Developer's Guide (2025)

Learn how to build web animations that run at 60fps on every device. Covers GPU acceleration, the rendering pipeline, Lighthouse optimization, and common pitfalls.

About 5 min read

Web Animation Performance — The Complete Developer's Guide (2025)

Nothing kills a beautiful animation faster than jank. A 60fps animation feels like glass. A 30fps animation feels like a PowerPoint. A janky 15fps animation makes users question whether their device is broken.

This guide explains exactly why animations get slow, how the browser renders motion, and the specific techniques that keep animations buttery smooth — even on mid-range mobile devices.


Understanding the Browser Rendering Pipeline

Before you can optimize animations, you need to understand what the browser does to display each frame.

The Pixel Pipeline

Every frame goes through up to five stages:

JavaScript → Style → Layout → Paint → Composite
  1. JavaScript — Your JS runs (event handlers, animations, RAF callbacks)
  2. Style — The browser recalculates CSS styles for changed elements
  3. Layout — Geometry is recalculated (positions, sizes). Most expensive step.
  4. Paint — Pixels are drawn into layers
  5. Composite — Layers are assembled and sent to the screen

The key insight: Skipping Layout and Paint is the goal. If you animate properties that only affect the Composite step, the browser skips the two most expensive stages.

The Magic Properties

Only two CSS properties skip both Layout and Paint:

PropertyWhat it doesGPU-Accelerated
transformMove, scale, rotate, skew✅ Yes
opacityTransparency✅ Yes

Everything else — width, height, top, left, background-color, border-width, font-size — triggers Layout or Paint (or both), causing the browser to re-render large portions of the page.


The 60fps Target

Your display refreshes at 60Hz (most devices) or 90–120Hz (modern phones). That means you have 16.67ms to render each frame at 60fps.

The browser needs about 6ms of that budget for its own overhead, leaving you ~10ms for JavaScript and rendering work.

If any frame takes longer than 16.67ms, you drop a frame. The user sees a stutter.

Measuring Frame Rate

// Manual FPS counter
let lastTime = performance.now();
let frames = 0;

function tick(now: number) {
  frames++;
  if (now - lastTime >= 1000) {
    console.log(`${frames} fps`);
    frames = 0;
    lastTime = now;
  }
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

Or use Chrome DevTools:

  1. Open DevTools → Performance tab
  2. Click the ⚙ icon → Enable "FPS meter"
  3. The live FPS counter appears in the top-right of the viewport

The 10 Golden Rules of Animation Performance

Rule 1: Only Animate transform and opacity

This cannot be overstated. Every other animated property costs you.

/* ❌ SLOW — triggers Layout */
.box {
  animation: slide-wrong 0.3s ease;
}
@keyframes slide-wrong {
  from { left: -100px; }
  to   { left: 0; }
}

/* ✅ FAST — GPU only */
.box {
  animation: slide-right 0.3s ease;
}
@keyframes slide-right {
  from { transform: translateX(-100px); }
  to   { transform: translateX(0); }
}

Rule 2: Promote Heavy Animations to Their Own Layer

.animation-heavy {
  will-change: transform, opacity;
  /* Or the older hack: */
  transform: translateZ(0);
}

will-change tells the browser to promote the element to its own compositor layer before the animation starts, avoiding the cost of layer creation mid-animation.

Warning: Don't apply will-change to everything. Each promoted layer consumes GPU memory. On a page with 50 animated elements, this can cause memory pressure and actually slow things down.

Rule of thumb: Only use will-change on elements that animate continuously (backgrounds, looping effects) or are about to animate (add class on hover, remove after transition).

Rule 3: Use requestAnimationFrame for JS Animations

Never animate with setInterval or setTimeout. These don't sync to the display refresh rate.

// ❌ Jank city
setInterval(() => {
  element.style.transform = `translateX(${x++}px)`;
}, 16);

// ✅ Smooth as butter
function animate(timestamp: number) {
  element.style.transform = `translateX(${position}px)`;
  position += speed;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Rule 4: Avoid Forced Synchronous Layout (Layout Thrashing)

Layout thrashing happens when you read a layout property (triggering layout calculation) then immediately write one (invalidating the calculation), in a loop.

// ❌ Triggers layout on every iteration
elements.forEach(el => {
  const height = el.offsetHeight; // READ — forces layout
  el.style.height = `${height + 10}px`; // WRITE — invalidates layout
});

// ✅ Batch reads then writes
const heights = elements.map(el => el.offsetHeight); // All reads first
elements.forEach((el, i) => {
  el.style.height = `${heights[i] + 10}px`; // Then all writes
});

Libraries like FastDOM can help automate this batching.

Rule 5: Debounce Scroll and Resize Handlers

Scroll fires up to 60× per second. Resize fires on every pixel change. Heavy work inside these handlers destroys performance.

// ❌ Runs expensive work 60x per second
window.addEventListener('scroll', () => {
  expensiveAnimation();
});

// ✅ Throttled to ~10x per second
let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      expensiveAnimation();
      ticking = false;
    });
    ticking = true;
  }
});

Rule 6: Use content-visibility for Off-Screen Content

For pages with lots of content below the fold, content-visibility: auto tells the browser to skip rendering off-screen sections entirely — including their animations.

.page-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Estimated height */
}

This can reduce initial render time by 40–70% on content-heavy pages.

Rule 7: Pause Animations When Not Visible

Animations running in inactive tabs waste battery and CPU. Use the Page Visibility API to pause them:

document.addEventListener('visibilitychange', () => {
  const els = document.querySelectorAll('.animated');
  els.forEach(el => {
    el.style.animationPlayState =
      document.hidden ? 'paused' : 'running';
  });
});

Rule 8: Avoid filter: blur() on Large Elements

filter: blur() is GPU-accelerated — but it's one of the most expensive GPU operations. A blur filter on a full-viewport element causes the GPU to process every pixel.

If you need blur backgrounds (glassmorphism), use it on small elements or apply it to a copy of the background rather than the background itself:

/* ❌ Expensive — blur applied to large background */
.hero {
  background: url(image.jpg);
  filter: blur(20px);
}

/* ✅ Better — blur applied to a small overlay */
.glass-card {
  backdrop-filter: blur(20px);
  /* backdrop-filter only blurs what's *behind* the element */
}

Rule 9: Choose the Right Easing Function

Easing functions affect how smooth an animation feels — even at the same FPS.

/* Built-in easings */
animation-timing-function: ease;           /* standard */
animation-timing-function: ease-in-out;    /* natural */
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring-like */

/* CSS spring (new in 2024) */
animation-timing-function: linear(
  0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938, 1.017,
  1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.157, 1.145 34%, 1.007 45.2%,
  0.988, 0.979, 0.975 53.8%, 0.975 55%, 0.978 58%, 1
);

For UI interactions (buttons, menus, modals), use ease-out — it feels instant at the start and settles smoothly at the end. This matches how physical objects decelerate.

Rule 10: Respect prefers-reduced-motion

Not an optimization per se, but critical for accessibility and a signal of craft:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

In React/Next.js, check for this preference in JS too:

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (!prefersReducedMotion) {
  startAnimation();
}

Measuring Animation Performance with Lighthouse

Lighthouse scores animations under these metrics:

  • Total Blocking Time (TBT) — Long animation JS tasks that block the main thread
  • Cumulative Layout Shift (CLS) — Layout shifts caused by animated elements
  • Largest Contentful Paint (LCP) — If your animated hero delays the main content

Fixing CLS from Animations

Layout shift during animation is a common LCP/CLS problem. If your animated element changes size during the animation, reserve its space upfront:

.animated-card {
  /* Reserve space before animation starts */
  min-height: 300px;
  contain: layout; /* Prevents animation from affecting surrounding elements */
}

Fixing LCP from Animated Heroes

If your hero section has a full-screen video or canvas animation, the LCP element (usually the H1 headline) can be delayed while the animation initializes. Fix this by:

  1. Rendering the headline with position: relative; z-index: 10 above the animation
  2. Lazy-loading the animation background
  3. Using a static poster image as an ::before pseudo-element that fades out once the animation loads

Debugging Jank: A Systematic Approach

When an animation stutters, follow this 5-step diagnostic:

Step 1: Open the Performance Panel

DevTools → Performance → Click Record → Reproduce the jank → Stop recording

Step 2: Find Long Frames

Look for frames taller than the 16ms line in the flame chart. These are your dropped frames.

Step 3: Identify the Cause

Click on a long frame to see which tasks took time:

  • Recalculate Style → You're changing too many CSS properties
  • Layout → You're animating non-transform properties
  • Paint → You have expensive paint operations (shadows, gradients on non-layers)
  • Script → Your JavaScript is doing too much per frame

Step 4: Check the Layers Panel

DevTools → Rendering → Paint flashing (shows what's being repainted in red) DevTools → Layers (shows compositor layers — too many is also bad)

Step 5: Fix and Retest

Common fixes:

SymptomFix
Animating left/topSwitch to transform: translate()
Constant full-page repaintAdd will-change: transform to animated elements
JS animation jankMove to CSS animations or Web Animations API
Layout thrashing in scroll handlerThrottle with RAF + batch reads/writes
Canvas animation dropping framesReduce particle count, use OffscreenCanvas

Quick Reference Card

ALWAYS SAFE TO ANIMATE:
  ✅ transform: translate, scale, rotate, skew
  ✅ opacity

SAFE WITH GPU PROMOTION (will-change):
  ✅ filter (on small elements)
  ✅ backdrop-filter

AVOID ANIMATING:
  ❌ width, height, min/max-width/height
  ❌ top, left, right, bottom
  ❌ margin, padding
  ❌ border-width
  ❌ background-color (use opacity layers instead)
  ❌ font-size, line-height
  ❌ box-shadow (expensive — use opacity on a pseudo-element)

TOOLS:
  Chrome DevTools Performance panel
  Chrome DevTools Rendering → FPS Meter
  Chrome DevTools Layers panel
  Lighthouse (Core Web Vitals)

Production-Ready Animations Without the Guesswork

Optimizing animations from scratch requires deep knowledge of the rendering pipeline, browser quirks, and device capabilities. The Animation Web library does this work for you.

Every template is:

  • Benchmarked at 60fps on mid-range Android devices
  • Only uses transform and opacity (unless otherwise noted)
  • Includes prefers-reduced-motion fallbacks
  • Lighthouse-tested for zero CLS impact

Browse the Animation Web Library and ship performant, beautiful animations today.

logo

AnimationWeb

A collection of components for your startup business or side project.

© 2026 AnimationWeb. All rights reserved.