Browser-based animation work lives or dies on timing, restraint, and how much layout pain you create under the hood. A beautiful easing curve means nothing if it triggers layout recalculation on every frame and drops to 15fps on a mid-range phone. In this guide, I break down how to build performant UI animations using CSS, SVG, and lightweight JavaScript, with examples drawn from real front-end pattern work and production performance testing. We cover the rendering pipeline, compositor-friendly properties, frame budget management, paint reduction, testing methodology, and the trade-offs that determine whether an animation helps or hurts the experience.

The web.dev performance guides provide excellent deep dives into Core Web Vitals and rendering performance metrics that complement the animation-specific focus here.

The Rendering Pipeline

Every frame your browser renders goes through a pipeline: JavaScript evaluation, Style calculation, Layout, Paint, and Composite. Animation performance depends on how far down this pipeline your animated property forces the browser to go.

Layout-triggering properties like width, height, margin, padding, top, and left force the browser to recalculate the geometry of affected elements and their neighbors. Animating these properties is expensive and almost never necessary for visual motion.

Paint-triggering properties like background-color, color, box-shadow, and border-radius do not affect layout but force the browser to repaint pixels. This is cheaper than layout but still significant for large or complex elements.

Compositor-only properties are transform and opacity. These can be animated entirely on the GPU compositor thread without touching layout or paint. They are the foundation of performant animation.

The 16ms Frame Budget

At 60fps, you have 16.67ms per frame. The browser needs some of that time for its own housekeeping, so your animation work gets roughly 10ms to 12ms per frame in practice.

If your animation triggers layout recalculation, that alone can consume 5ms to 20ms on complex pages. Add paint time and you are already over budget. This is why compositor-only animations are not just a best practice but a hard requirement for smooth motion on real devices.

Test at throttled CPU speeds. Chrome DevTools lets you apply 4x or 6x CPU throttle, which approximates mid-range mobile device performance. An animation that feels smooth on your development machine might stutter badly under throttle.

Transform-Based Motion

Anything you might animate with top/left positioning can almost always be done with transform: translate(). Anything you might animate with width/height can often be done with transform: scale(). And rotation-based animation uses transform: rotate(), which is inherently compositor-friendly.

The key insight is that transform-based animation moves the element’s visual representation without changing its layout box. The browser does not recalculate surrounding elements, which is why transforms are so much cheaper.

There is a subtlety here: promoting an element to its own compositor layer (via will-change: transform or transform: translateZ(0)) tells the browser to render that element separately. This makes future transform animations cheaper but has a memory cost for each promoted layer. Do not promote everything. Promote elements that will actually animate.

Managing Paint Regions

When an animation does trigger paint (because you are animating color, shadow, or another paint property), you can limit the damage by isolating the painting region. CSS contain: paint or contain: layout paint tells the browser that changes inside an element do not affect anything outside it. This allows the browser to limit repaint to the contained region.

For box-shadow animations, consider using a pseudo-element with the shadow and animating its opacity instead of animating the shadow properties directly. A shadow at full opacity versus a shadow at zero opacity is a compositor-only change, while animating box-shadow blur or spread triggers paint on every frame.

JavaScript Animation Patterns

For animations driven by JavaScript, always use requestAnimationFrame rather than setInterval or setTimeout. RAF synchronizes with the browser’s refresh cycle and pauses when the tab is not visible, saving resources.

The pattern for a JavaScript animation loop:

function animate(timestamp) {
  // Calculate progress based on elapsed time
  // Apply transform or opacity changes
  // Request next frame if animation is not complete
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Use elapsed time, not frame count, to drive animation progress. Frame rates vary across devices, so a time-based approach ensures consistent animation speed everywhere.

The Web Animations API offers a middle ground between CSS animations and full JavaScript control. It provides keyframe-based animation with JavaScript timing control and is now well-supported across modern browsers.

SVG Animation Specifics

SVG transforms behave slightly differently from CSS transforms. The default transform origin for SVG elements is the element’s coordinate system origin (usually 0,0 of the viewBox), not the element’s center. Set transform-origin explicitly or use transform-box: fill-box to make SVG transforms origin from the element’s bounding box.

Path animation using stroke-dasharray and stroke-dashoffset is a paint operation, but it is generally efficient because stroke rendering is optimized in most browsers. For long or complex paths, performance can degrade. Test with your actual path data, not just simple examples.

Reduced Motion

The prefers-reduced-motion media query must be respected. Users who set this preference have a reason, ranging from vestibular sensitivity to personal preference. The minimum response is to disable all non-essential animation. A better response is to replace motion with instant state changes (opacity snaps, color changes) that communicate the same information without movement.

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

This blanket approach works but is blunt. For more control, selectively disable specific animations while keeping functional transitions.

Testing Methodology

Performance testing for animation requires more than just watching it on your screen. Use Chrome DevTools Performance panel to record animation sequences and look for:

  • Frames exceeding 16ms total time
  • Layout events during animation frames
  • Paint events during compositor-only animation (indicating a missed optimization)
  • Layer count growth (indicating over-promotion)

Test on real devices when possible. Emulation helps but cannot fully replicate the rendering behavior of actual mobile GPUs and CPUs.

Frequently Asked Questions

Should I use CSS transitions or CSS animations? Transitions are better for state changes (hover, class toggle). Animations are better for continuous or looping motion. Both compile to the same compositor-friendly operations for transform and opacity.

Does will-change actually help? Yes, for elements that are about to animate. Apply it just before animation starts and remove it after. Do not leave it on permanently, as it forces layer promotion and increases memory use.

How many simultaneous animations are too many? There is no fixed number. The constraint is total frame budget. Five simple transform animations are cheaper than one complex shadow animation. Profile, do not guess.