The real bottleneck in CSS versus JavaScript animation

A recurring question in front-end development is whether CSS animations are inherently faster than JavaScript-based ones. The common assumption is that CSS always wins on performance. But the real difference isn't about the language or the rendering engine's ability to compute values — it's about where the animation runs.

Consider a simple bouncing-ball effect. You could build it with a CSS keyframe animation:

@keyframes bounce {
  to {
    transform: translateX(calc(var(--bounce-magnitude) * -1));
  }
}

.ball {
  --bounce-magnitude: 200px;
  animation: bounce 1000ms infinite alternate;
}

Or you could drive it from JavaScript with a requestAnimationFrame loop:

const startTime = performance.now();

const ball = document.querySelector('.ball');

function animate() {
  const elapsedTime = performance.now() - startTime;

  // ✂️ Calculate `x` based on the amount of time that has passed.

  ball.style.transform = `translateX(${x}px)`;

  window.requestAnimationFrame(animate);
}

Both approaches produce visually identical motion. The JavaScript version does extra work each frame — calculating a value and touching the DOM — but modern engines handle that in well under a millisecond, even on modest hardware. That overhead isn't what causes jank.

The decisive factor is thread placement. CSS keyframe animations and transitions run on a separate compositor thread. JavaScript, however, executes on the main thread, sharing time with everything else your application does: framework reconciliation, fetch response parsing, event handlers, layout calculations. When that thread is busy, a requestAnimationFrame-based animation simply doesn't get its callback in time, and the animation stutters or freezes. CSS animations on the compositor thread are isolated from that contention.

Why some JavaScript libraries don't have this problem

Not all JavaScript animation libraries suffer from the main-thread limitation. Libraries that build on the Web Animations API (WAAPI) get the best of both worlds. WAAPI is a JavaScript interface that hooks into the browser's native animation engine — the same one that powers CSS keyframes. Animations started through WAAPI run off the main thread, just like pure CSS animations.

This explains why a library like Motion (formerly Framer Motion) keeps animations smooth even when the main thread is saturated, while a library like GSAP, which uses its own JavaScript-driven ticker, will stall under the same conditions. Neither choice is a mistake: GSAP offers a breadth of features and control that isn't easily achieved through WAAPI, and it accepts the main-thread trade-off to get there. Motion prioritizes compositor-thread performance.

Choosing the right approach

The practical guidance boils down to preference and constraints. When CSS can express the animation — transitions, keyframes, view transitions, linear() easing, scroll-driven animations — it remains the most robust option because it never competes with application logic. When a use case requires JavaScript-level control, reach for a WAAPI-based library first, since it avoids the main-thread drawback without sacrificing the scripting layer. Libraries with their own render loop are still viable, but you must account for their sensitivity to main-thread load. Knowing the mechanism behind each tool tells you which one to use when the browser gets busy.