It is not about calculation speed
The usual framing is that JavaScript animation is slow because the browser has to run code on every frame. That sounds right, but it misses the actual reason CSS wins.
A modern JavaScript engine can run a simple animation loop in a fraction of a millisecond per frame. Math is not the bottleneck. The bottleneck is the thread that math runs on.
JavaScript runs on the main thread. So does layout, style recalculation, event handling, and every fetch callback you fire. When any of that work piles up, your animation loop has to wait its turn. The frame is late, and the eye reads late frames as jank.
CSS animations run somewhere else
CSS transitions and keyframe animations can run on the compositor thread, separate from the main thread. Once the browser knows the start state, the end state, and the timing, it can hand the whole animation off and let it play without touching JavaScript again.
That is the headline advantage. If your main thread freezes for 200ms parsing a big JSON response, a JavaScript animation stutters for 200ms. A compositor-driven CSS animation keeps playing, smooth, because it does not live on the thread that froze.
The catch is that only some properties get this treatment. transform and opacity are the safe pair: they composite without triggering layout or paint. Animating width, top, or margin forces layout on every frame and drags the work back onto the main thread, CSS or not. Animate transforms, not box geometry, and you stay on the fast path.
The platform got a lot more capable
A few years ago the honest answer was that CSS handled the easy cases and you reached for a library the moment things got interesting. That line has moved.
Scroll-driven animation now has a native timeline, so you can tie an animation to scroll position with no scroll listener and no IntersectionObserver. The linear() easing function lets you describe springy, multi-step easing curves that used to need a JavaScript tween. The View Transitions API animates between two DOM states, including across full page loads, with the browser doing the crossfade and morph for you.
Each of those replaces a chunk of code you used to ship. That matters for weight too: a general-purpose animation library is real bytes on the wire. Skipping it is faster to load and one less dependency to maintain.
When JavaScript still wins
CSS is the default, not the answer to everything. There are real cases where a JavaScript library earns its place.
SVG shape morphing, where one path smoothly bends into a different path, is the clearest one. CSS cannot interpolate between arbitrary path shapes, so a library like Motion or GSAP that drives the Web Animations API is the right tool. The same goes for physics that has to react to live input, like a draggable card that flings with momentum and settles with a spring tuned to velocity. Anything where the animation has to respond frame by frame to something only JavaScript knows about belongs in JavaScript.
The rule of thumb: if the motion is a known transition between two states, CSS. If the motion has to be computed live from input or data, JavaScript.
What to reach for first
Default to CSS. It runs off the main thread, it survives a busy page, and it ships zero bytes of library code. Keep your animations on transform and opacity so they stay on the compositor.
Reach for a JavaScript animation library when you hit a wall the platform genuinely cannot clear: shape morphing, velocity-driven physics, or motion that has to be calculated on the fly. That is a much shorter list than it used to be.