Myth Busting: CSS Animations vs. JavaScript

Avatar of Jack Doyle
Jack Doyle on (Updated on )

The following is a guest post by Jack Doyle, author of the GreenSock Animation Platform (GSAP). Jack does a lot of work with animations in the browser and has discovered that the generic opinion that “CSS is faster” just isn’t true. It’s more than that, as well. I’ll let him explain.

Once upon a time, most developers used jQuery to animate things in the browser. Fade this, expand that; simple stuff. As interactive projects got more aggressive and mobile devices burst onto the scene, performance became increasingly important. Flash faded away and talented animators pressed HTML5 to do things it had never done before. They needed better tools for complex sequencing and top-notch performance. jQuery simply wasn’t designed for that. Browsers matured and started offering solutions.

The most widely-acclaimed solution was CSS Animations (and Transitions). The darling of the industry for years now, CSS Animations have been talked about endlessly at conferences where phrases like “hardware accelerated” and “mobile-friendly” tickle the ears of the audience. JavaScript-based animation was treated as if it was antiquated and “dirty”. But is it?

As someone who’s fascinated (bordering on obsessed, actually) with animation and performance, I eagerly jumped on the CSS bandwagon. I didn’t get far, though, before I started uncovering a bunch of major problems that nobody was talking about. I was shocked.

This article is meant to raise awareness about some of the more significant shortcomings of CSS-based animation so that you can avoid the headaches I encountered, and make a more informed decision about when to use JS and when to use CSS for animation.

Lack of independent scale/rotation/position control

Animating the scale, rotation, and position of an element is incredibly common. In CSS, they’re all crammed into one “transform” property, making them impossible to animate in a truly distinct way on a single element. For example, what if you want to animate “rotation” and “scale” independently, with different timings and eases? Maybe an element is continuously pulsing (oscillating scale) and you’d like to rotate it on rollover. That’s only possible with JavaScript.

See the Pen Independent Transforms by GreenSock (@GreenSock) on CodePen

In my opinion, this is a glaring weakness in CSS but if you only do simpler animations that animate the entire transform state at any given time, this won’t be an issue for you.

Performance

Most comparisons on the web pit CSS animations against jQuery since it is so pervasive (as if “JavaScript” and “jQuery” were synonymous) but jQuery is widely known to be quite slow in terms of animation performance. The newer GSAP is also JavaScript-based but it’s literally up to 20x faster than jQuery. So part of the reason JavaScript animation got a bad reputation is what I call the “jQuery factor”.

The most frequently cited reason for using CSS for animation is “hardware acceleration”. Sounds yummy, right? Let’s break it down into two parts:

GPU involvement

The GPU is highly optimized for tasks like moving pixels around and applying transform matrices and opacity, so modern browsers try to offload those tasks from the CPU to the GPU. The secret is to isolate the animated elements onto their own GPU layers because once a layer is created (as long as its native pixels don’t change), it’s trivial for the GPU to move those pixels around and composite them together. Instead of calculating every single pixel 60 times per second, it can save chunks of pixels (as layers) and just say “move that chunk 10 pixels over and 5 pixels down” (or whatever).

Side note: It’s not wise to give every element its own layer because GPUs have limited video memory. If you run out, things will drastically slow down.

Declaring your animations in CSS allows the browser to determine which elements should get GPU layers, and divvy them up accordingly. Super.

But did you know you can do that with JavaScript too? Setting a transform with a 3D characteristic (like translate3d() or matrix3d()) triggers the browser to create a GPU layer for that element. So the GPU speed boost is not just for CSS animations – JavaScript animation can benefit too!

Also note that not all CSS properties get the GPU boost in CSS animations. In fact, most don’t. Transforms (scale, rotation, translation, and skew) and opacity are the primary beneficiaries. So don’t just assume that if you animate with CSS, everything magically gets GPU-juiced. That simply isn’t true.

Offloading calculations to a different thread

The other part of “hardware acceleration” has to do with being able to use a different CPU thread for animation-related calculations. Again, this sounds great in theory but it doesn’t come without costs, and developers often overestimate the benefits.

First of all, only properties that don’t affect document flow can truly be relegated to a different thread. So again, transforms and opacity are the primary beneficiaries. When you spin off other threads there’s overhead involved with managing that process. Since graphics rendering and document layout eat up the most processing resources (by FAR) in most animations (not calculating the intermediate values of property tweens), the benefit of using a separate thread for interpolation is minimal. For example, if 98% of the work during a particular animation is graphics rendering and document layout, and 2% is figuring out the new position/rotation/opacity/whatever values, even if you calculated them 10 times faster, you’d only see about a 1% speed boost overall.

Performance comparison

The stress test below creates a certain number of image elements (dots) and animates them from the center to random positions around the edges using random delays, creating a starfield effect. Crank up the number of dots and see how jQuery, GSAP, and Zepto compare. Since Zepto uses CSS transitions for all of its animations, it should perform best, right?

See the Pen Speed Test: GSAP vs Zepto (CSS Transitions) vs jQuery by GreenSock (@GreenSock) on CodePen

The results confirm what is widely reported on the web – CSS animations are significantly faster than jQuery. However, on most devices and browsers I tested, the JavaScript-based GSAP performed even better than CSS animations (by a wide margin in some cases, like on the Microsoft Surface RT GSAP was probably at least 5 times faster than the CSS transitions created by Zepto, and on the iPad 3 iOS7 transforms were significantly faster when animated with GSAP instead of CSS transitions):

Animated properties Better w/JavaScript Better w/CSS
top, left, width, height Windows Surface RT, iPhone 5s (iOS7), iPad 3 (iOS 6), iPad 3 (iOS7), Samsung Galaxy Tab 2, Chrome, Firefox, Safari, Opera, Kindle Fire HD, IE11 (none)
transforms (translate/scale) Windows Surface RT, iPhone 5s (iOS7), iPad 3 (iOS7), Samsung Galaxy Tab 2, Firefox, Opera, IE11 iPad 3 (iOS6), Safari, Chrome
Exactly how much “better”? The original version of the test had a frames-per-second counter for quantifiable results, but it quickly became apparent that there’s no truly accurate way to measure FPS across browsers especially with CSS animations, and certain browsers were reporting misleading numbers, so I removed it. You can easily gauge relative performance, though, by cranking up the number of dots, switching among engines, and watching how things perform (smooth movement, steady timing and dot dispersion, etc.). After all, the goal is to have animations look good.

Interesting things to note:

  • When animating top/left/width/height (properties that affect document flow), JavaScript was faster across the board (GSAP, not jQuery).
  • A few devices seemed highly optimized for transforms whereas others handled top/left/width/height animations better. Most notably, the older iOS6 was much better with CSS-animated transforms, but the newer iOS7 flip-flopped and now they are significantly slower (I blogged about it here).
  • There’s a substantial lag in the initial animation startup with CSS animations as the browser calculates layers and uploads the data to the GPU. This also applies to JavaScript-based 3D transforms, so “GPU acceleration” doesn’t come without its own costs.
  • Under heavy pressure, CSS transitions were more likely to spray out in bands/rings (this appears to be a synchronization/scheduling issue, possibly due to them being managed in a different thread).
  • In some browsers (like Chrome), when there were a very high number of dots animating, it completely killed the opacity fade of the text, but only when using CSS animations!

Although well-optimized JavaScript is often just as fast if not faster than CSS animations, 3D transforms do tend to be faster when animated with CSS, but that has a lot to do with the way browsers handle 16-element matrices today (forcing conversion from numbers to a concatenated string and back to numbers). Hopefully that’ll change, though. In most real-world projects, you’d never notice the performance difference anyway.

I’d encourage you to do your own testing to see which technology delivers the smoothest animation in your particular project(s). Don’t buy the myth that CSS animations are always faster, and also don’t assume that the speed test above reflects what you’d see in your apps. Test, test, test.

Runtime controls and events

Some browsers allow you to pause/resume a CSS keyframes animation, but that’s about it. You cannot seek to a particular spot in the animation, nor can you smoothly reverse part-way through or alter the time scale or add callbacks at certain spots or bind them to a rich set of playback events. JavaScript provides great control, as seen in the demo below.

See the Pen Impossible with CSS: controls by GreenSock (@GreenSock) on CodePen

Modern animation is very much tied to interactivity, so it’s incredibly useful to be able to animate from variable starting values to variable ending ones (maybe based on where the user clicks, for example), or change things on-the-fly but declarative CSS-based animation can’t do that.

Workflow

For simple transitions between two states (i.e. rollovers or expanding menus, etc.), CSS Transitions are great. For sequencing things, however, you generally need to use CSS keyframe animations which force you to define things in percentages, like:

@keyframes myAnimation {
  0% {
    opacity: 0;
    transform: translate(0, 0);
  }
  30% {
    opacity: 1;
    transform: translate(0, 0);
  }
  60% {
    transform: translate(100px, 0);
  }
  100% {
    transform: translate(100px, 100px);
  }
}
#box {
   animation: myAnimation 2.75s;
}

But when you’re animating, don’t you think in terms of time rather than percentages? Like “fade up the opacity for 1 second, then slide to the right for 0.75 seconds, and bounce down to a rest 1 second later”. What happens if you spend hours crafting a complicated sequence in percentages, and then the client says “make that part in the middle 3 seconds longer”? Ouch. You’d need to recalculate ALL of the percentages!

Usually building animations involves a lot of experimentation, especially with timing and eases. This is actually where a seek() method would be quite useful. Imagine building out a 60-second animation piece-by-piece and then finessing the final 5 seconds; you would need to sit through the first 55 seconds every time you want to see the results of your edits to the last parts. Yuck. With a seek() method, you could just drop that into place during production to skip to the part you’re working on, and then remove it when you’re done. Big time-saver.

It is becoming increasingly common to animate canvas-based objects and other 3rd-party library objects but unfortunately CSS animations can only target DOM elements. That means that if you invest a lot of time and energy in CSS animations, it won’t translate to those other types of projects. You’ll have to switch animation tool sets.

There are a few more workflow-related conveniences that are missing in CSS Animations:

  • Relative values. Like “animate the rotation 30 degrees more” or “move the element down 100px from where it is when the animation starts”.
  • Nesting. Imagine being able to create animations that can get nested into another animation which itself can be nested, etc. Imagine controlling that master animation while everything remains perfectly synchronized. This structure would promote modularized code that is much easier to produce and maintain.
  • Progress reporting. Is a particular animation finished? If not, exactly where is it at in terms of its progress?
  • Targeted kills. Sometimes it’s incredibly useful to kill all animations that are affecting the “scale” of an element (or whatever properties you want), while allowing the rest to continue.
  • Concise code. CSS keyframe animations are verbose even if you don’t factor in all the redundant vendor-prefixed versions necessary. Anyone who has tried building something even moderately complex will attest to the fact that CSS animations quickly get cumbersome and unwieldy. In fact, the sheer volume of CSS necessary to accomplish animation tasks can exceed the weight of a JavaScript library (which is easier to cache and reuse across many animations).

Limited effects

You can’t really do any of the following with CSS animations:

  • Animate along a curve (like a Bezier path).
  • Use interesting eases like elastic or bounce or a rough ease. There’s a cubic-bezier() option, but it only allows 2 control points, so it’s pretty limited.
  • Use different eases for different properties in a CSS keyframe animation; eases apply to the whole keyframe.
  • Physics-based motion. For example, the smooth momentum-based flicking and snap-back implemented in this Draggable demo.
  • Animate the scroll position
  • Directional rotation (like “animate to exactly 270 degrees in the shortest direction, clockwise or counter-clockwise”).
  • Animate attributes.

Compatibility

CSS-based animation doesn’t work in IE9 and earlier. Most of us hate supporting older browsers (especially IE), but the reality is that some of us have clients who require that support.

Browser prefixes are necessary for many browsers, but you can leverage preprocessing tools to avoid having to manually write them out.

Conclusion

Are CSS animations “bad”? Certainly not. In fact, they’re great for simple transitions between states (like rollovers) when compatibility with older browsers isn’t required. 3D transforms usually perform very well (iOS7 being a notable exception), and CSS animations can be very attractive for developers who prefer putting all of their animation and presentation logic in the CSS layer. However, JavaScript-based animation delivers far more flexibility, better workflow for complex animations and rich interactivity, and it often performs just as fast (or even faster) than CSS-based animation despite what you may have heard.

When compared to jQuery.animate(), I can understand why CSS Animations were so appealing. Who in their right mind wouldn’t jump at the chance to get a 10-fold performance boost? But it’s no longer a choice between jQuery and CSS Animations; JavaScript-based tools like GSAP open up entirely new possibilities and wipe out the performance gap.

This article isn’t about GSAP or any particular library; the point is that JavaScript-based animation doesn’t deserve a bad reputation. In fact, JavaScript is the only choice for a truly robust, flexible animation system. Plus, I wanted to shed some light on the frustrating parts of CSS animations (which nobody seems to talk about) so that you can ultimately make a more informed decision about how you animate in the browser.

Will the Web Animations spec solve things?

The W3C is working on a new spec called Web Animations that aims to solve a lot of the deficiencies in CSS Animations and CSS Transitions, providing better runtime controls and extra features. It certainly seems like a step forward in many ways, but it still has shortcomings (some of which are probably impossible to overcome due to the need for legacy support of existing CSS specifications, so for example, independent transform component control is unlikely). That’s a whole different article, though. We’ll have to wait and see how things come together. There are definitely some smart guys working on the spec.