How to achieve smooth 60fps CSS animations without jank
16ms frame budget, transform vs positional properties, opacity animation, will-change, GPU layer promotion, containment, requestAnimationFrame for JS animations
Smooth CSS Animations
At 60fps the browser has 16.67ms per frame. Exceed this budget and a frame is dropped โ visible as a stutter or jank. The browser's rendering work (style, layout, paint, composite) must fit within that window alongside your JavaScript.
The rule: animate only transform and opacity. These are the only properties that can be promoted to GPU compositor threads and animated without involving the main thread at all.
/* Fade in โ composite only */
.fade-in {
animation: fade 0.3s ease-out;
}
@keyframes fade {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scale on hover โ composite only */
.card:hover {
transform: scale(1.03);
transition: transform 0.2s ease;
}If you must animate a layout property (e.g., height for an accordion), use the View Transition API or the FLIP technique (First, Last, Invert, Play) to fake layout changes using transform:
// FLIP: record start position, jump to end, invert with transform, animate to identity
const first = el.getBoundingClientRect();
el.classList.add('expanded');
const last = el.getBoundingClientRect();
const deltaY = first.top - last.top;
el.style.transform = `translateY(${deltaY}px)`;
requestAnimationFrame(() => {
el.style.transition = 'transform 0.3s ease';
el.style.transform = '';
});Enable compositing hints for elements you know will animate:
.modal { will-change: transform, opacity; }