We’re Gonna Need A Bigger API!

Advertisement

Everyone likes stuff that moves about on the Web, right? Remember how you cried joyful tears when you first used <marquee>? I do. I nearly sobbed all the water out of my body as I gazed upon “JAKE’S COOL WEBSITE” bobbing back and forth in uppercase serif. Of course, we’re more mature as an industry these days.

We’ve learned that users don’t want websites to look like a CSI console having a personal crisis; instead, we go for smooth transitions that enhance the experience, rather than being the experience themselves. In terms of animation APIs, we’ve been poorly catered to, leaving us to hack around with timers that weren’t really built for animation. Things have been steadily improving in that area, but the new Web Animation specification1 looks set to shake things up a lot.

So, why do we need a new animation spec? Don’t we have enough ways to animate things already?

Optimizing the way to makes things move2
Optimizing the way to make things move. (Image source3)

Let’s Animate A Thing!

Imagine we wanted to animate something horizontally from one left position to another, over three seconds, and then do something on completion. We can do this without JavaScript, using CSS animations184, but if the start and end positions are programmatically determined, then we’ll need something that we can control from script.

Using requestAnimationFrame

If you’re performing visual updates with JavaScript, then you should be using requestAnimationFrame. It synchronizes itself to real screen updates, giving you as much time as possible to get everything ready for rendering. If the browser is on a 60 Hz screen (most are) and your frames can be constructed in less than a 60th of a second, then you’ll get 60 frames per second (FPS). requestAnimationFrame prevents you creating frames that don’t have time to display. Synchronizing to the screen’s rate is important; 30 FPS looks smoother than 40 FPS because 40 doesn’t divide into the screen’s native 60 Hz. HTML5 Rocks has a great article on syncing to the screen5.

Unfortunately, jQuery uses setInterval6, which isn’t as smooth as requestAnimationFrame. requestAnimationFrame doesn’t trigger while the tab or window isn’t visible, which is A Good Thing™. Unfortunately, this has created backwards incompatibility with websites that rely on setInterval’s less optimal behavior of continuing to run in the background. You can opt into requestAnimationFrame via a plugin7. Go and add that to all of your pages using jQuery animation now — I promise to wait for you — just make sure that switching tabs doesn’t break your animations.

Anyway, enough chatting. Here’s a simple animation using raf8, moving a box horizontally from 250px to 500px. Note that the box starts at 0px, so there’s a jump to 250px when the animation starts; this proves we can start the animation from a point other than its current rendered position.

Here’s the code:

// On button press…
animateLeft(elm, '250px', '500px', function() {
  console.log("Done!");
});

// The implementation
function animateLeft(elm, from, to, done) {
  // Turn our CSS values into numbers
  // We're being lazy and assuming they're in px
  from = parseInt(from, 10);
  to = parseInt(to, 10);
  // Work out the amount we need to move the box
  var diff = to - from;

  var duration = 3000;
  var startTime = performance.now();

  // Set initial position
  elm.style.transform = 'translate(' + from + 'px, 0)';

  function frame(time) {
    // How long has the animation been running?
    var animTime = time - startTime;
    // Are we done?
    if (animTime >= duration) {
      // It's likely that the last rendered position wasn't the
      // final position, so we set it here.
      elm.style.transform = 'translate(' + to + 'px, 0)';

      done();
    }
    else {
      // What position should the box be in?
      var position = from + (animTime / duration * diff);
      elm.style.transform = 'translate(' + position + 'px, 0)';
      // Request our next frame
      requestAnimationFrame(frame);
    }
  }
  // request our first frame
  requestAnimationFrame(frame);
}

The above is the ideal according-to-specification code. In the working example, I had to deal with vendor prefixes on requestAnimationFrame and transform. We’re animating using transform and translate, rather than left, because they allow for subpixel positioning and, therefore, smoother animation9, one of the advantages that Flash had over HTML for so long.

This is a pretty large and stinky chunk of code to simply animate a thing, and it would get a lot larger if we handled differing CSS units and easing. Of course, you could stick all of the complicated bits in a library and give yourself a simpler API. Here’s the frame-by-frame breakdown:

image02-50010

This is the timeline view of Chrome Developer Tools while the animation is running. Each frame executes some JavaScript, recalculates the style and layout, paints the box, and then sends that to the GPU, which composites it to the page. The draw time spikes a few times, resulting in a jolt in the animation. This is caused by delays in interacting with the GPU (the gray spikes) or delays caused by other JavaScript (the yellow spikes).

This highlights a performance bottleneck of JavaScript-driven animation:

image01-50011

Here, another bit of JavaScript does some stuff and takes 250 milliseconds to do it. While this is happening, our animation can’t move. In the real world, this could be a social-media button waking up and doing something slow, or it could be some of your own script triggered by a user interaction. In the example above, I made a button that performs a while loop for 250 milliseconds12 (I’m pretty sure this code is in every social-media button). If you press it during the animation, it will block the animation and look nasty.

I recently sung the praises of requestAnimationFrame for animating canvas13, so why am I hatin’ on it now? JavaScript-driven animations aren’t a bad practice — they give you full control frame by frame and pixel by pixel when combined with <canvas> — but returning to JavaScript land 60 times a second is overkill for DOM animations that have a defined start and end. Ideally, we want to tell the browser all about our animation and leave it to do its thing, while we get on with something else.

Of course, we kinda have this already.

Using CSS Transitions

.whatever {
   transform: translate(250px, 0);
   transition: transform 3s linear;
}
.whatever:hover {
   transform: translate(500px, 0);
}

CSS transitions and animations let the browser make all kinds of optimizations because it knows the end point of the animation. They’re not blocked by JavaScript on some platforms, such as Chrome for Android and desktop Chrome with threaded compositing enabled in about:flags (expect threaded compositing to arrive in more browsers).

Let’s script it!

function animateLeft(elm, from, to, done) {
  // Set initial position
  elm.style.transform = 'translate(' + from + ', 0)';
  // Define the transition type
  elm.style.transition = 'all 3s linear';

  function transitionEnd(event) {
    // Beware of bubbled events
    if (event.target != elm) { return; }
    // Clear the transition
    elm.style.transition = '';
    // We don't want that listener firing for future anims
    elm.removeEventListener('transitionend', transitionEnd);

    done();
  }

  // Listen for end of transition
  elm.addEventListener('transitionend', transitionEnd);
  // start the transition
  elm.style.transform = 'translate(' + to + ', 0)';
}

Here’s a live example14. It’s much simpler than our raf example, but a bug has crept in. The from is ignored; the animation starts from the element’s current position, even though we’ve explicitly set it to something else. Why?

// Set initial position
elm.style.transform = 'translate(' + from + ', 0)';
// Define the transition type
elm.style.transition = 'all 3s linear';
// …and later…
// Start the transition
elm.style.transform = 'translate(' + to + ', 0)';

Changing properties in the style object doesn’t change the element’s computed style. The style is computed only when the browser needs to know the impact that those styles will have on the page (for example, when the element needs to be drawn). The element doesn’t need to be drawn between the two assignments to elm.style.transform, so the first assignment is ignored.

Of course, we can hack it:

// Set initial position
elm.style.transform = 'translate(' + from + ', 0)';
// Abracadabra!
elm.offsetWidth;
// Define the transition type
elm.style.transition = 'all 3s linear';
// …and later…
// start the transition
elm.style.transform = 'translate(' + to + ', 0)';

offsetWidth returns the rendered width of an element, including padding. To calculate this, the browser needs to take into account all of the styles on the page, including the transform that we set for the initial position. That works. Check out the live example15.

image03-50016

Performance is steady at 60 FPS. And we can see that each frame is a simple composite; all of the heavy lifting is farmed out to the GPU.

However, relying on offsetWidth to force the element into its starting position is hacky, and it’s conceivable that a future browser release will find a way to optimize out the reflow, breaking our hack.

Reflows are not without cost either:

image00-50017

The Developer Tools warn us about this use of offsetWidth, because the browser calculates a layout that it never draws. The test page is very basic, so the layout cost is cheap, but things can be very different in the real world.

So, is there a less hacky, more reliable way?

Enter CSS Animations

CSS animations184 have explicit keyframe values. Let’s script them:

function animateLeft(elm, from, to, done) {
  // Create a style element for our animation
  var style = document.createElement('style');
  // Generate a unique name
  var animName = 'anim' + Date.now() + Math.floor(Math.random() * 10000);

  // Build the CSS
  style.textContent = '' +
    '@keyframes ' + animName + ' { ' +
      'from { ' +
        'transform: translate(' + from + ', 0);' +
      '}' +
      'to {'
        'transform: translate(' + to + ', 0);' +
      '}' +
    '}';

  // Add it to the page
  document.head.appendChild(style);

  function transitionEnd(event) {
    // Beware of bubbled events
    if (event.target != elm) { return; }
    // Clear the animation
    elm.style.animation = '';
    // Clean up the DOM
    document.head.removeChild(style);
    // Retain the final position
    elm.style.transform = 'translate(' + to + ', 0)';
    // We don't want that listener firing for future anims
    elm.removeEventListener('animationend', transitionEnd);

    done();
  }

  // Listen for end of transition
  elm.addEventListener('animationend', transitionEnd);

  // Start the animation
  elm.style.animation = animName + ' 3s linear forwards';
}

Ugh! All of that just to move a thing? It works19, but all of that DOM work is heavy-handed for what we’re trying to achieve. Also, if an animation is cancelled halfway through (for example, if the animation style is changed), then animationend will not fire — meaning that our done callback won’t fire or, worse, it’ll fire at the end of some future unrelated animation. There is no animationcancel event.

Web Animations, Save Us From This Mess!

It’s early days for the Web Animations specification20, but it’s pretty exciting. It brings a boatload of animation performance and synchronization features natively to the DOM that JavaScript libraries currently have to hack their way through.

The specification itself is kinda terrifying. My heart sank as I opened the page and watched the scrollbar get smaller and smaller. But, thankfully, most of it is implementation detail.

Here’s how we’d script our animation in the brave new world of Web Animation:

// Set our start position
elm.style.transform = 'translate(250px, 0)';
// Animate to the end position
var anim = elm.animate({
  transform: 'translate(500px, 0)'
}, 3);
// Do something on completion
anim.onend = function() {
  console.log('Done!');
};

Here, elm is an HTMLElement. The API is intuitive, especially if you’ve created animations with something like jQuery.

Like CSS animations and transitions, it gives the browser the full story up front, so we get all of the same optimizations without having to dynamically build CSS. Web Animations solves this by allowing us to tell the browser the full story of what we’re going to do. Then, the browser can go off and animate things itself.

Web Animations give us the scripting API to browser-driven animation that’s sorely missing. Above is the “Hello world” example. The specification21 includes advanced easing, path-based animation, parallelization, synchronization, interrupting and adapting, all in a way that the browser can take away from JavaScript land and optimize accordingly.

It’s still very early days, so don’t throw out your animation libraries yet. But if you want to experiment with the new API and provide feedback, a polyfill is tracking22 the rapidly evolving spec. Exciting times!

(al)

Footnotes

  1. 1 https://dvcs.w3.org/hg/FXTF/raw-file/tip/web-anim/index.html
  2. 2 http://www.smashingmagazine.com/wp-content/uploads/2013/03/intro.jpg
  3. 3 http://www.smashingmagazine.com/2011/09/14/the-guide-to-css-animation-principles-and-examples/
  4. 4 https://developer.mozilla.org/en-US/docs/CSS/Tutorials/Using_CSS_animations
  5. 5 http://www.html5rocks.com/en/tutorials/speed/rendering/
  6. 6 https://github.com/jquery/jquery/blob/caac041fcc31724b8b579939e8053966559483ca/src/effects.js#L692
  7. 7 https://github.com/gnarf37/jquery-requestAnimationFrame
  8. 8 http://jsbin.com/iliket/14/quiet
  9. 9 http://paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/
  10. 10 http://www.smashingmagazine.com/wp-content/uploads/2013/02/image02.png
  11. 11 http://www.smashingmagazine.com/wp-content/uploads/2013/02/image01.png
  12. 12 http://jsbin.com/iliket/21/quiet
  13. 13 http://calendar.perfplanet.com/2012/snow-in-canvas-land/
  14. 14 http://jsbin.com/iliket/16/quiet
  15. 15 http://jsbin.com/iliket/17/quiet
  16. 16 http://www.smashingmagazine.com/wp-content/uploads/2013/02/image03.png
  17. 17 http://www.smashingmagazine.com/wp-content/uploads/2013/02/image00.png
  18. 18 https://developer.mozilla.org/en-US/docs/CSS/Tutorials/Using_CSS_animations
  19. 19 http://jsbin.com/iliket/18/quiet
  20. 20 https://dvcs.w3.org/hg/FXTF/raw-file/tip/web-anim/index.html
  21. 21 https://dvcs.w3.org/hg/FXTF/raw-file/tip/web-anim/index.html
  22. 22 https://github.com/web-animations/web-animations-js

↑ Back to topShare on Twitter

Jake is a Developer Advocate at Google who's keen on Web performance. He developed Sprite Cow to help ease the pain of sprite sheets. Jake started a blog way after blogs stopped being cool.

Advertising

Note: Our rating-system has caused errors, so it's disabled at the moment. It will be back the moment the problem has been resolved. We're very sorry. Happy Holidays!

  1. 1

    Great article.
    But I don’t think having a bigger API will solve animations variable frame rate. As long as animations have multiple dependencies, they’ll always lack the consistency; how busy is your CPU when the animation starts? How much memory you have left? How many scripts/tasks are running in the background? Latency, bottlenecks, wrong code practices, etc.
    And all this assuming we’re using a single browser.

    For now, I’m keeping my animations to the strict necessary.

    • 2

      True, the API won’t directly solve variable framerate, but using an animation-aware API (requestAnimationFrame, CSS animations, Web Animations) will allow the browser to optimise for this better in the future.

      We’ve bounced around some thoughts around adaptive frame rates, eg if the animation isn’t steadily managing to meet the refresh rate of the screen (mostly 60fps), it’ll drop down to 1/2 refresh rate for a bit. If those frames are significantly under budget, it could step up to the refresh rate. Early thinking stages at the moment, but we might be able to come up with something better than the highest-possible-framerate model we’re using right now.

  2. 3

    http://github.com/lvivski/anima has pretty similar API and can do both, use rAF and generate pure CSS animations

  3. 4

    I’m excited about the new API and I agree that it could help performance. However, I think it still leaves some gaps that only scripted animation systems like GSAP (http://www.greensock.com/gsap-js/) can address. For example, what about animating along a Bezier path or animating custom (non-DOM) objects/properties or directional rotation or special eases like SlowMo, Elastic, etc.? It’s a big step in the right direction, though.

    I had the same reaction when I first opened the API spec and the scrollbar got tiny. But I was encouraged to see that the spec is following GSAP’s current functionality pretty closely, at least conceptually. Everything is synchronized, time-based, easily reversible and seek-able (if that were a word), nestable, etc.

    When the new API gets implemented in browsers, it’ll likely get used inside GSAP anyway, thus allowing GSAP to deliver compatibility with older browsers while delivering the speed boost when possible. We’re hyper concerned with top-notch performance (see http://www.greensock.com/js/speed.html for a comparison), so this will be a welcome addition. Thanks for bringing attention to it. I’d be curious to know if you’ve had a chance to dig into GSAP yet. It’s relatively new to the JS scene, but it has been a standard in Flash for years. Anyway, thanks again for shedding light on the proposed Web Animation spec.

    • 5

      The spec covers path based animation (including bezier) as a subclass of AnimationEffect, or will when it’s fleshed out (https://dvcs.w3.org/hg/FXTF/raw-file/tip/web-anim/index.html#the-pathanimationeffect-interface).

      Directional rotation is already covered by CSS, where animating to 360deg is different to animating to -360deg.

      For easing, something like elastic isn’t possible in the current spec without chaining animations together. There’s a note to look into extending SplineTimingFunction (https://dvcs.w3.org/hg/FXTF/raw-file/tip/web-anim/index.html#idl-def-SplineTimingFunction) to allow curves with more than 4 items, I hope they do that sooner rather than later.

      As for non-DOM properties, yeah, the Web Animation API is a scripting API to SVG & CSS animation, you can only animate things those can animate. For other properties you’ll have to use requestAnimationFrame.

      I did quite a bit of Flash development in my student days so I’m familiar & a big fan of GASP (thanks!), I haven’t played with the JS version much, but delighted to see it uses requestAnimationFrame rather than getting bogged down with minor compatibility issues like jQuery did.

      I don’t see Web Animations replacing things like GASP, but more that it would allow GASP to take particular animations entirely off-thread.

  4. 6

    Ribhararnus Pracutian

    March 5, 2013 9:12 pm

    Why not use http://ricostacruz.com/jquery.transit/ , It’s very simple jquery plugin I think.

    • 7

      It’s not simple, look at the source. It’s having to rely on the sort of hacks I covered in the “Using CSS Transitions” section & lacks the kind of control that Web Animations offers. That’s not the plugin’s fault, it’s the lack of a good browser API.

  5. 8

    Simon W. Jackson

    March 6, 2013 5:36 am

    This API can come fast enough.

  6. 9

    Have you checked out jQuery libraries like:

    https://github.com/benbarnett/jQuery-Animate-Enhanced
    and
    https://github.com/louisremi/jquery.transition.js

    that swap to using CSS3 transitions for animations when they are supported, and use normal jQuery animate when not supported?

    Seems like a good approach (particularly if they are updated as specifications progress)…

    Cheers
    Matt

  7. 10

    I read the first paragraph and had to comment. Yes, I too cried tears of joy when I first used on my first website!

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top