Web Animations Without Killing Performance
Animations enhance user experience when they run smoothly. Here's how to build web animations that feel fluid without causing jank or layout thrashing.
Strategic Systems Architect & Enterprise Software Developer
Why Animations Jank
Animation jank — the visible stutter or choppiness when a transition does not run at a consistent 60 frames per second — happens for one reason: the browser cannot complete its rendering work within the 16.67ms budget that 60fps requires. Understanding why that budget gets exceeded is the key to building smooth animations.
The browser's rendering pipeline has distinct phases: JavaScript execution, style calculation, layout, paint, and composite. Each animated property triggers different phases of this pipeline. Animating a property that requires layout (like width, height, top, left, margin, or padding) forces the browser to recalculate the position of potentially hundreds of elements on every single frame. Animating a property that requires paint (like background-color, box-shadow, or border-radius) is cheaper but still expensive. Animating a property that only requires compositing — transform and opacity — is nearly free because the GPU handles it without touching the layout or paint phases.
This is not a minor optimization. The difference between animating left: 0 to left: 200px and animating transform: translateX(0) to transform: translateX(200px) is the difference between 40% frame drops and zero frame drops on a mid-range device. The visual result is identical. The performance difference is enormous.
The rule: animate only transform and opacity whenever possible. If you need to animate color, size, or position properties that do not map to transforms, consider whether the animation is worth the performance cost — on mobile devices, it often is not.
CSS Animations and Transitions Done Right
CSS transitions are the simplest animation mechanism and should be your default choice for state changes: hover effects, menu openings, modal appearances, and theme switches. They perform well because the browser can optimize them onto the GPU when you animate the right properties.
.card {
transform: translateY(0);
opacity: 1;
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
.card:hover {
transform: translateY(-4px);
}
.card.entering {
transform: translateY(20px);
opacity: 0;
}
This hover effect and entrance animation both use only transform and opacity, so they run entirely on the compositor thread. No layout recalculation, no paint. The browser can run these at 60fps even on low-powered devices.
For more complex sequences, CSS @keyframes animations provide multi-step control:
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.sidebar {
animation: slideIn 0.4s ease-out forwards;
}
Use will-change sparingly and intentionally. Adding will-change: transform tells the browser to promote an element to its own compositing layer, which can improve animation performance but consumes GPU memory. Apply it only to elements you know will animate, and remove it after the animation completes. Do not add will-change to every element on the page — that wastes memory and can actually degrade performance by creating too many compositing layers.
JavaScript Animations: When and How
CSS handles most UI animations well, but some scenarios require JavaScript: animations driven by scroll position, physics-based motion, animations that respond to user input in real-time, and complex orchestrated sequences.
When you need JavaScript animations, use requestAnimationFrame (rAF) exclusively. Never animate with setTimeout or setInterval — they are not synchronized with the browser's rendering cycle and will produce jank. RAF calls your animation function precisely once before each repaint, giving you a consistent frame budget.
function animate(element, startTime) {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / 500, 1);
element.style.transform = `translateX(${progress * 200}px)`;
element.style.opacity = String(1 - progress * 0.5);
if (progress < 1) {
requestAnimationFrame(() => animate(element, startTime));
}
}
RequestAnimationFrame(() => animate(el, performance.now()));
For scroll-driven animations, the new CSS Scroll Timeline API handles many cases without JavaScript. For cases that still require JavaScript, use IntersectionObserver to trigger animations when elements enter the viewport rather than listening to scroll events. Scroll event listeners fire dozens of times per second and can block the main thread, causing both animation jank and general page unresponsiveness.
The Web Animations API (WAAPI) offers a middle ground — JavaScript control with browser-optimized execution:
element.animate(
[
{ transform: 'scale(0.95)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
],
{ duration: 300, easing: 'ease-out', fill: 'forwards' }
);
WAAPI animations run on the compositor thread when possible, giving you JavaScript control with CSS-level performance. They are also cancellable and reversible, making them ideal for interactive animations.
Accessibility and Motion Preferences
Not every user wants motion. Some users experience vestibular disorders that make animation physically uncomfortable — nausea, dizziness, and disorientation. The prefers-reduced-motion media query lets you respect user preferences:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This is not optional — it is a WCAG 2.1 requirement (Success Criterion 2.3.3). Any animation that plays automatically should respect this preference. Interactive animations triggered by user action are less critical but should still be reduced or eliminated for users who request it.
For decorative animations — background particles, floating elements, parallax scrolling — provide an explicit toggle in addition to the system preference. Not every user who dislikes excessive animation has changed their OS setting.
Performance is also an accessibility concern. Animations that cause the main thread to block for 100ms+ make the page unresponsive to input. A user clicking a button during a heavy animation may experience no visible response, leading them to click again and potentially triggering duplicate actions. Keeping animations off the main thread is not just a performance best practice — it is a usability requirement.
Keep animations purposeful. An entrance animation that draws attention to important content helps the user. A continuous pulsing animation on every card component is distracting. Motion should guide attention, provide feedback, and communicate state changes. If an animation does not serve one of those purposes, it is decoration — and decoration that costs performance is a poor trade.