Why CSS Animations Are Preferred Over JavaScript Animations Under Load
CSS animations run on the compositor thread off the main thread, allowing them to stay smooth even when the browser is busy loading resources, executing scripts, or painting, while JavaScript animations on requestAnimationFrame stutter under the same conditions.
When the browser is under heavy load, animation performance becomes a critical user experience concern. The emilkowalski/skills repository establishes clear guidelines for choosing between CSS and JavaScript animations, grounded in browser architecture and real-world performance characteristics.
Thread Architecture: The Core Performance Divide
Modern browsers use a multi-threaded rendering model that fundamentally separates animation workloads.
CSS Animations Run Off the Main Thread
CSS animations are handled by the compositor thread, which operates independently of JavaScript execution. According to skills/review-animations/STANDARDS.md, this architectural decision means "CSS animations beat JS under load — they run off the main thread; rAF-based animations stutter while the browser loads/scripts/paints"【https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L123】.
Even when the main thread is blocked by:
- DOM manipulation and layout recalculation
- Network request processing
- Heavy JavaScript computation
- Image decoding and painting
The compositor thread continues ticking at 60fps (or the display's refresh rate).
JavaScript Animations Are Main-Thread Bound
JavaScript-driven animations typically rely on requestAnimationFrame, which queues callbacks on the main thread. When that thread is contested, frame deadlines are missed and jank occurs — visible stuttering that degrades perceived performance.
Five Architectural Advantages of CSS Animations Under Load
1. Thread Separation
The compositor thread receives only the final committed styles and transform matrices. It can interpolate between states without consulting JavaScript, insulating animation smoothness from main-thread pressure.
2. Predictable Timing
The browser's rendering engine manages the animation timeline directly. This eliminates variance from garbage collection pauses, event handler execution, or third-party script interference.
3. Lower Runtime Overhead
CSS animations require a one-time style rule declaration:
/* styles.css — parsed once, hardware-accelerated thereafter */
.fade-in {
opacity: 0;
animation: fadeIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
to { opacity: 1; }
}
<!-- index.html -->
<div class="fade-in">Welcome!</div>
Each frame of a JavaScript animation incurs function call overhead, style recalculation, and potential layout thrashing:
// script.js — executed every frame, main-thread bound
const el = document.querySelector('.js-fade');
let start = null;
function step(timestamp) {
if (!start) start = timestamp;
const progress = Math.min((timestamp - start) / 300, 1);
el.style.opacity = progress; // triggers style recalc
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
4. Interruptibility Without Complexity
CSS transitions support retargeting — if a property is already transitioning, new values interpolate from the current intermediate state rather than restarting.
/* styles.css */
.toggle {
transition: background-color 0.2s ease-out;
}
.toggle.active {
background-color: #4caf50;
}
// script.js — rapid clicks smoothly retarget the transition
document.querySelector('.toggle').addEventListener('click', e => {
e.target.classList.toggle('active');
});
As documented in STANDARDS.md, this behavior differs from CSS keyframe animations, which restart from zero when retriggered. For rapid UI state changes, transitions or springs are preferred over keyframes【https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md#L79】.
5. Performance Guarantees
Modern browsers enforce that compositor-thread animations do not block scrolling, input events, or repaints. This preserves interactivity during heavy loading periods that would freeze JavaScript-driven effects.
When to Use JavaScript Animations Despite the Trade-off
The repository's guidance in skills/pick-ui-library/SKILL.md and skills/emil-design-eng/SKILL.md establishes a pragmatic decision framework:
| Use CSS animations | Use JavaScript/spring libraries |
|---|---|
| Predetermined, declarative motion | Physics-based or gesture-driven interactions |
| Opacity, transform, and filter changes | Motion parameters that react to runtime state |
| Simple state transitions | Complex orchestration between multiple elements |
| Load-critical UI paths | Dynamic spring configurations |
For example, Framer Motion or similar libraries remain appropriate when animations must respond to continuous input (drag gestures, scroll velocity) or when spring physics require runtime parameter adjustment.
Practical Implementation Guidelines
Prioritize CSS transforms and opacity for maximum hardware acceleration. These properties avoid layout recalculation and can be promoted to compositor layers with will-change:
.optimized {
will-change: transform, opacity;
transform: translate3d(0, 0, 0); /* force layer creation */
}
Remove will-change after animation completion to free GPU memory:
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
Summary
- CSS animations execute on the compositor thread, remaining smooth when the main thread is overloaded with loading, scripting, or painting tasks.
requestAnimationFrameanimations stall under main-thread pressure, producing visible jank that degrades user experience.- Thread separation, predictable timing, lower overhead, and built-in interruptibility make CSS the default choice for standard UI motion.
- Reserve JavaScript animation libraries for dynamic, input-driven, or physics-based scenarios where runtime parameter control is essential.
Frequently Asked Questions
Can CSS animations ever hurt performance?
Yes. Animating layout properties (width, height, top, left) forces synchronous layout recalculation on the main thread, eliminating the off-thread advantage. Stick to transform and opacity for guaranteed compositor-only work.
How do I debug whether an animation is running on the compositor?
Open Chrome DevTools Performance panel, start a recording, and look for green bars in the Frames track. Purple bars indicate main-thread work. The Layers panel shows which elements have been promoted to compositor layers.
What about requestIdleCallback for JavaScript animations?
requestIdleCallback explicitly deprioritizes work until the main thread is idle — the opposite of what smooth animations require. It is unsuitable for visual updates and intended for non-urgent background tasks like analytics batching.
Do CSS animations work with server-side rendering or static site generators?
Yes. CSS animations are declarative and portable — the stylesheets render identically whether served from a CDN, generated at build time, or hydrated from a JavaScript framework. This makes them particularly valuable for performance-critical initial page loads.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →