Why animations matter—and why you should care about the cost
We live in an era where users expect interfaces to respond with immediacy. Subtle motion helps users understand changes in state, confirms actions, and guides attention. But the moment an animation isn’t thoughtfully crafted, it can become a stray pebble in a performance funnel—slowing down interactions, increasing battery drain, and making the experience feel laggy.
The tricky part is not whether animations are useful, but where they add value and where they become a maintenance burden. In the browser, motion isn’t free. Each animation touches a chain of steps: style calculation, layout, painting, and compositing. When you animate properties that trigger layout (like width, height, margin, padding), you’re inviting reflows that ripple through the page. When you animate properties that only affect composition (like transform and opacity), you’re leveraging a smoother path that most modern GPUs can handle more efficiently.
Think of it like a budget line item: every animation should have a reason, a scope, and a measurable impact on the user’s perception of speed. The best animations are those that are informational and incidental—improving clarity without demanding more CPU cycles than the rest of the page needs.
The browser pipeline you’re optimizing for
When an animation runs, the browser goes through a familiar loop: it rebuilds styles for the affected elements, performs layout calculations, paints pixels to layers, and composes those layers into the final bitmap you see on screen. If you animate a property that affects layout, you trigger reflow: the entire page recalculates its geometry. If you animate only transforms or opacity, the browser often uses a separate compositing layer for that element, meaning it can redraw only a tiny subset of the screen.
A disciplined approach is to target the slower path only when absolutely necessary and keep the fast path busy with transform-based animations. This is not a universal rule—sometimes a transform alone isn’t enough for a desired effect—but it’s the best starting point for most interactive UI details.
Tip: remember that “GPU-accelerated” does not guarantee instant performance. GPU work can still get backed up if you flood the pipeline with expensive paints. A smooth experience is a balanced one.
CSS vs. JS animations: when to reach for which tool
CSS animations are declarative, usually easier to reason about, and often optimized by the browser. They shine for simple, state-driven transitions like hover reveals, toggling a class, or a subtle float. JavaScript animations, driven by requestAnimationFrame or the Web Animations API, offer precise timing control, dynamic behavior, and complex choreography.
Practical guidance:
- Prefer CSS for simple, stateful transitions: color fade, slide-in, or hover micro-interactions. Use transform and opacity as your primary animated properties. They’re typically cheaper because they enable compositing.
- Use the Web Animations API or requestAnimationFrame for complex timelines: when an animation depends on real-time data, user input, or syncs with multiple elements. The API makes it easier to pause, reverse, or synchronize with other events.
- Avoid layout-thrashing properties in animations: animating width, height, margin, padding, left, or top tends to reflow the entire document and kills frame rates on mid-range devices.
Example comparisons:
.card {
transform: translateY(0);
transition: transform .25s ease;
}
.card:hover {
transform: translateY(-6px);
}
// JS: synchronized pulse using requestAnimationFrame
function pulse(el) {
let t = 0;
function frame(ts) {
t += 0.016;
el.style.transform = 'scale(' + (1 + Math.sin(t) * 0.03) + ')';
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
pulse(document.querySelector('.card'));
The CSS example is easy to maintain and very cheap for a single state change. The JS example gives you more control but requires careful lifecycle management to avoid runaway CPU use or memory leaks.
Performance budgets and how to measure them
A practical way to keep animations honest is to treat performance as a feature you ship. Set expectations with your team: how long should an animation take? How often can it repaint? What’s the maximum number of compositing layers you’re willing to create?
Tools matter. Chrome DevTools Performance panel can show you frame-by-frame painting, scripting time, and the cost of layout. Lighthouse audits can flag long tasks and unoptimized paint. The key is to measure in the context of real user journeys, not isolated snippets.
- Enable “Preserve Log” and record a typical user interaction. Watch for long tasks (over 50-100 ms).
- Check the timeline for layout thrashes: a forced reflow often looks like waves in the layout bar after a burst of animation.
- Inspect composited layers: too many layers can actually slow things down due to overdraw. Remove or consolidate when possible.
A simple rule of thumb: if you can achieve the same visual goal without changing layout and you still get a pleasing effect, prefer that path. If you must animate layout, isolate the animation to a small, bounded region and avoid affecting the rest of the document flow.
A practical guide for delicious, efficient motion
Let’s walk through a concrete scenario: a product card in an e-commerce grid that responds to hover with a quick lift and a detail reveal. The goal is to feel responsive without pushing the page into a heavy paint cycle.
- Choose your motion path: a slight vertical lift with a fade-in of secondary content. Keep the primary card on its own compositing layer.
- Apply transform and opacity for the animation. Reserve layout changes for user-triggered content changes, not for the hover effect.
- Limit the animation scope: apply the effect only to the card and its immediate children, not to the entire grid.
- Ensure a clean end state: the content should return to its baseline quickly and without a layout shift.
Implementation sketch:
/* CSS approach (low risk) */
.card {
will-change: transform, opacity;
transform: translateY(0);
opacity: 1;
transition: transform .25s ease, opacity .25s ease;
}
.card:hover {
transform: translateY(-6px);
opacity: 0.98;
}
.card-details {
opacity: 0;
transform: translateY(6px);
transition: opacity .25s ease, transform .25s ease;
}
.card:hover .card-details {
opacity: 1;
transform: translateY(0);
}
If you need something more dynamic, consider the Web Animations API for synchronized timing between multiple elements:
const details = document.querySelector('.card-details');
const lift = details.animate(
[
{ opacity: 0, transform: 'translateY(6px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{
duration: 250,
fill: 'both'
}
);
document.querySelector('.card').addEventListener('mouseenter', () => lift.play());
document.querySelector('.card').addEventListener('mouseleave', () => lift.reverse());
In practice, you’ll often start with CSS for its simplicity, then layer in Web Animations API for complex sequences or when you need to toggle multiple elements in perfect sync.
Accessibility and motion: the essential duty
Respect users who prefer reduced motion. It’s not merely polite—it’s a legal and accessibility best practice in many contexts. Use a media query to simplify or disable motion when the user indicates reduced motion.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
}
Design your defaults to be calm and deterministic. If animation conveys critical state information, provide a non-animated fallback, or ensure that the information is also conveyed through non-animated means (e.g., a visible state change, text, or an ARIA-live region for updates).
Testing and debugging your motion loop
Make animation an ongoing, testable part of your UI workflow. Practical checks:
- Run the page on mid-range devices and ensure 60 FPS is consistently achievable for your critical interactions.
- Use the Performance tab to identify long frames and check if you’re repainting large swaths of the DOM every frame.
- Audit layers with the Layers panel to avoid excessive layering. If a single element creates more than a few layers, it’s worth revisiting.
- Test in both light mode and dark mode if color changes accompany motion, as visual contrast can affect perceived latency.
A practical debugging trick: isolate the animation in a small sandbox (a separate page or a reduced component), measure the cost, then port the leanest version back to the real UI with confidence.
A compact checklist for motion-minded teams
- Prefer transform and opacity for most animations; avoid layout-affecting properties unless necessary.
- Keep animations short, purposeful, and visually distinct from layout changes.
- Limit the number of concurrently animating elements to avoid GPU saturation.
- Use will-change strategically. Don’t overdo it; only on elements you’re actively animating.
- Respect reduced motion preferences and provide non-animated fallbacks when appropriate.
- Measure with real user paths, not only isolated demos. Watch for long tasks and reflows in production-like conditions.
- Document the animation decisions, so future maintainers understand the trade-offs and the intended user experience.
Closing thoughts: motion as a feature, not a garnish
Web animations can make interfaces feel alive — but only if they’re designed with performance as a first-class constraint. The goal isn’t to ban motion; it’s to ensure every motion earns its keep by clarifying state, guiding attention, or delighting users without stealing cycles from the tasks that matter most. When you adopt a disciplined mindset—favoring compositing paths, measuring impact, and honoring accessibility—you’ll ship experiences that feel fast, responsive, and human. The browser is fast by default, but motion is where you decide how fast your app genuinely feels.
Subscribe to my newsletter to get the latest updates and news
Member discussion