Build smooth 60fps animations that don't jank. Covers CSS animations vs. JavaScript, compositor-only properties, requestAnimationFrame, FLIP technique, and the patterns that keep animations fluid on every device.
Web animation performance is the practice of delivering smooth, responsive, and efficient animations in the browser, where the difference between a delightfully fluid interface and one that stutters or janks can define user perception of quality. It matters when interactions are frequent—hover effects, modal transitions, scroll-driven reveals, micro-interactions, and full-page animations—especially on devices with limited processing power, such as mobile phones or low-end tablets.
Animations serve three primary goals: visual feedback, spatial continuity, and perceived performance. Each goal demands specific performance characteristics, and misalignment between animation intent and implementation leads to visible degradation.
Visual feedback requires animations to begin within 100ms of a user action. To achieve this, animations must start *before* the browser receives the full event data.
requestAnimationFrame (rAF) to synchronize animation with the display refresh cycle.transform and opacity properties for layout-friendly animations, as they trigger only compositor-level updates.// Optimal animation loop for visual feedback
function animate(time) {
// Update animation state
const progress = time / 1000; // assuming 1s animation duration
element.style.transform = `translateX(${progress * 100}px)`;
element.style.opacity = 1 - progress;
// Schedule next frame
requestAnimationFrame(animate);
}
// Start animation immediately after user input
element.addEventListener('click', () => {
const startTime = performance.now();
requestAnimationFrame(animate);
// No delay; animation begins in the next frame
});
Animations that rely on width, height, or margin trigger *layout thrashing*—recomputing layout for every frame—leading to stuttering. To avoid this, ensure layout changes are batched by using transform and opacity only.
Spatial continuity is achieved when animations preserve the user’s mental model of the interface across state changes. This requires consistent timing, easing, and visual alignment between elements.
transform: translate instead of left/top to avoid layout recalculation./* Shared animation properties for spatial continuity */
.slide-in {
animation: slide-in 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
}
@keyframes slide-in {
from {
transform: translateX(-100px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
A common failure is inconsistent timing between related animations. A modal opening while a button fades out may feel disconnected if their durations and easing differ. To ensure synchrony, use a shared timing context:
const timeline = new TimelineMax();
timeline
.to(button, 0.3, { opacity: 0, ease: 'sine.inOut' })
.to(modal, 0.3, { opacity: 1, x: 0, ease: 'sine.inOut' }, 0.1);
// Both animations start with a 0.1s delay between them
Perceived performance aims to mask delays—such as network fetches or heavy computation—by showing animations during the wait.
transform and opacity animations to show loading states.will-change to hint to the compositor which elements will change..loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.9);
will-change: transform, opacity;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.loading-overlay.active {
opacity: 1;
transform: scale(1);
}
/* Apply when data is loading */
function startLoading() {
const overlay = document.querySelector('.loading-overlay');
overlay.classList.add('active');
// Animation begins immediately
}
function finishLoading() {
const overlay = document.querySelector('.loading-overlay');
overlay.classList.remove('active');
// Animation completes, hiding overlay
}
The will-change property is often misunderstood. It must be applied *before* the animation begins. If set too late, the compositor may miss the opportunity to optimize.
Timing and synchronization are critical for animation quality. Poorly timed animations appear “off,” while unsynchronized ones feel disjointed.
Use performance.now() for precise timing.
const start = performance.now();
function animate(time) {
const elapsed = time - start;
const progress = Math.min(elapsed / 1000, 1);
element.style.transform = `translateX(${progress * 200}px)`;
element.style.opacity = 1 - progress;
requestAnimationFrame(animate);
}
// Start animation at exact moment of user interaction
element.addEventListener('touchstart', (e) => {
const startTime = performance.now();
requestAnimationFrame(animate);
// Animation starts at the first touch frame
});
When multiple animations run in parallel, use TimelineMax, GSAP, or Web Animations API to coordinate them. The Web Animations API provides a low-level, declarative interface for precise control.
const animation = element.animate([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(200px)', opacity: 0 }
], {
duration: 1000,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'both'
});
// Control animation programmatically
animation.pause();
animation.play();
animation.reverse();
Animations that affect layout—such as changing width, height, or padding—can cause jank due to reflow and repaint. The key to reducing jank is minimizing layout thrashing.
transform and opacity for animations that do not require layout updates.transform: translate instead of left/top.will-change: transform on elements that animate..card {
will-change: transform;
transition: transform 0.3s ease;
}
.card.hovered {
transform: translateX(10px) translateY(-5px);
}
When transform is combined with opacity, the browser can perform the animation entirely in the compositor, avoiding GPU and CPU bottlenecks.
Easing functions define how animation speed changes over time. Common easing functions include ease, ease-in, ease-out, ease-in-out, and cubic-bezier.
.ease-in-out {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.ease-in-out-elastic {
transition: transform 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
A frequent mistake is applying cubic-bezier(0.4, 0, 0.6, 1) without understanding how the control points affect motion. The cubic-bezier function is defined by two control points: (x1, y1) and (x2, y2).
.cubic-bezier(0.6, 0, 0.4, 1) {
/* Acceleration at start, deceleration at end */
}
requestAnimationFrame is called *after* the event, not at the event’s frame.// ❌ Common mistake: animation starts one frame late
element.addEventListener('click', () => {
requestAnimationFrame(() => {
element.style.transform = 'scale(1.1)';
});
});
// ✅ Correct: start animation at the event frame
element.addEventListener('click', () => {
const startTime = performance.now();
requestAnimationFrame(() => {
element.style.transform = 'scale(1.1)';
});
});
will-change: An element animates width, height, and padding, but will-change is not set. The browser recalculates layout for every frame..card {
width: 200px;
height: 150px;
padding: 10px;
}
.card.expanded {
width: 300px;
height: 250px;
padding: 20px;
}
Without will-change, the animation is slow and janky. Adding will-change: width, height, padding resolves it.
transform optimization: Using left and top for animation instead of transform. This forces layout updates on every frame./* ❌ Slow animation using left/top */
.element {
left: 0;
top: 0;
transition: left 0.5s, top 0.5s;
}
.element.moving {
left: 100px;
top: 50px;
}
/* ✅ Fast animation using transform */
.element {
transform: translate(0, 0);
transition: transform 0.5s;
}
.element.moving {
transform: translate(100px, 50px);
}
load events instead of DOMContentLoaded.// ❌ Fade-in animation starts too early
window.addEventListener('load', () => {
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
});
// ✅ Fade-in animation starts after content is ready
document.addEventListener('DOMContentLoaded', () => {
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
});
performance.measure to diagnose.const measure = performance.measure('animation-frame-timing', 'start', 'end');
console.log(measure.duration); // in milliseconds
Use the browser’s built-in profiler to identify animation bottlenecks.
Animation section in the timeline.Layout and Paint sections.requestAnimationFrame calls.Use console.time and console.timeEnd for manual profiling.
console.time('animation-start');
element.style.transform = 'translateX(100px)';
console.timeEnd('animation-start');
Check for jank using the window.requestAnimationFrame loop with performance.now().
let lastTime = 0;
function frame(time) {
const frameTime = time - lastTime;
if (frameTime > 16.6) { // > 60fps
console.warn(`Jank detected: ${frameTime.toFixed(2)}ms`);
}
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
requestAnimationFrame for animation loops.transform and opacity only.will-change on animated elements.cubic-bezier for custom easing.width, height, padding changes.performance.measure.performance.now() for precise timing.Animation performance is not just about smoothness—it is about aligning perception with action, and it is only achieved through deliberate, consistent, and measurable implementation.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy