PHOBOSLAB

Blog Home

What the requestAnimationFrame API Should Have Looked Like

Chrome, Firefox and IE10 now support requestAnimationFrame – a noble idea, with a totally backwards API.

Imagine you have this JavaScript animation that you want to constantly update. An animation that constantly updates, needs to be updated more than once, i.e. constantly. After all it's an animation. So why on earth do I have an API to request one update at a time?

Mozilla's documentation even warns developers that the API might not work as expected:

Note: Your callback routine must itself call requestAnimationFrame() unless you want the animation to stop.

Ah, yes.

Car analogy time: I have this very nice car that makes a beeping noise whenever I open the door with the ignition off and the headlights still on. You know, to warn me that my headlights might run the battery dry in my absence. "Thank you car, that's very kind of you. But let me ask you something: why didn't you just turn the headlights off yourself instead of notifying me?"

The requestAnimationFrame API is modeled after setTimeout when it should've been modeled after setInterval instead.

Here's my proposal:

// Install an animation that continously calls the callback
var animationId = setAnimation( callback, element );

// Stop the animation
clearAnimation( animationId );

And the code to make this happen:

(function(w) { 
    "use strict";
    
    // Find vendor prefix, if any
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for( var i = 0; i < vendors.length && !w.requestAnimationFrame; i++ ) {
        w.requestAnimationFrame = w[vendors[i]+'RequestAnimationFrame'];
    }
    
    // Use requestAnimationFrame if available
    if( w.requestAnimationFrame ) {
        var next = 1, 
            anims = {};
                
        w.setAnimation = function( callback, element ) {
            var current = next++;
            anims[current] = true;
            
            var animate = function() {
                if( !anims[current] ) { return; } // deleted?
                w.requestAnimationFrame( animate, element );
                callback();
            };
            w.requestAnimationFrame( animate, element );
            return current;
        };
        
        w.clearAnimation = function( id ) {
            delete anims[id];
        };
    }
    
    // [set/clear]Interval fallback
    else {
        w.setAnimation = function( callback, element ) {
            return w.setInterval( callback, 1000/60 );
        }
        w.clearAnimation = w.clearInterval;
    }
}(window));

Gist on Github

Monday, May 7th 2012

16 Comments:

#1Kevin Gadd – Monday, May 7th 2012, 00:20

Nice idea. Is writing your own requestAnimationFrame into window really a good move, though?

#2Dominic – Monday, May 7th 2012, 00:25

This is meant as a polyfill. It doesn't overwrite the requestAnimationFrame function if it's already present. Defining setAnimation and clearAnimation on the window certainly is a matter of taste (and compatibility) though.

#3Enrique Moreno Tent – Monday, May 7th 2012, 02:00

Your proposal has the same drawbacks that setInterval has over setTimeout. The update function might be called a second time, even if the last update step hasnt finished. For one, i never user setInterval, but a setTimeout that calls itself at the end. I think it makes good sense the way it is.

And for your car analogy, what if the purpose of the driver was to actually leave the lights on and the ignition off? (Not a common case, but it might happen) Wouldnt it be annyoing for the driver that the car wouldnt allow it?

I think the current behaviour is all-right.

#4Dominic – Monday, May 7th 2012, 04:14

This is a myth. JavaScript is single threaded. The next update can never be called when the previous update hasn't finished yet. There's no drawback of setInterval compared to setTimeout.

What's the use-case with the headlights? Burying dead bodies in the desert at night? :)

#5Brandon Jones – Monday, May 7th 2012, 06:03

I've actually run into quite a few cases where I use requestAnimationFrame as a one-shot call. Typically this is because I recognize some part of my UI needs to be redrawn (apply a new CSS class, etc). I do the actual draw inside the RAF callback to keep all of my drawing in sync across the app.

As such the current format works well for both that case and the times that I want to call it in a loop.

#6 – david – Monday, May 7th 2012, 06:53

Visual updates are meant to be fast, this is extra overhead that isn't needed, it's just sugar.

#7Dominic – Monday, May 7th 2012, 15:12

@Brandon: I'm not sure I understand - what do you gain by doing all your draws in the rAF callback for one-shot stuff? It really sounds like this complicates things a lot - you have to remember somewhere that you want to apply a new CSS class, instead of applying it immediately.

Without rAF, even if you do multiple draws for an event (e.g. mouse click), they will be all drawn at the same time when your event callback finishes. Using rAF may even postpone this draw for one frame.

I was under the impression that using rAF would have no gain for one-shot stuff - hence my proposal.

@david: I'd be surprised if the overhead is measurable at all. Also, sugar is important - without sugar we would still be writing assembler code :)

#8 – Ashley – Monday, May 7th 2012, 15:38

I remember Safari screwing up pretty hard if you use setInterval for a game that is running really slowly and the frames take longer than the interval. I think it was better to follow setTimeout to make sure that problem never happened... otherwise badly designed browsers might end up making really long queues of events to run.

#9Brandon Jones – Monday, May 7th 2012, 18:02

@Dominic: RAF has little benefit it you are only updating a single part of the UI, correct. A nice property that it has, though, is if you call RAF from several different places the browser wil synchronize all of their callbacks to happen on the same refresh cycle. That means for a large UI with many disparate parts putting your drawing code in RAF means that your entire UI will update in sync, and hence have less visual stutter, even if two components within that UI have no idea that the other one exists.

#10 – Marco Rogers – Monday, May 7th 2012, 18:57

@Dominic the difference with setInterval is that your next execution gets scheduled in the event loop as soon as possible when the delay expires, even if your last iteration has not completed. Yes js is single threaded so it won't actually execute until the last one finishes, but you can't stop it from executing because it's already scheduled. So the drawback is that if your jobs start taking longer than your delay, you will start stacking up interval executions and your interval timing will go to hell. SetTimeout let's you control this better by timing your executions and adjusting the delay on the next one accordingly.

That said, the above js implementation of setAnimatuon won't have this problem as it only calls animate recursively. The version that uses setInterval will though. I didn't take a hard look at this though, so please correct me if I'm missing something. It's an interesting technique and I tend to think both versions should be available.

#11Dominic – Monday, May 7th 2012, 19:43

@Brandon: ok, that makes sense from a technical standpoint, but I still can't imagine that this would have any visible effects for the user. I guess we just should have both methods available then.

@Marco: Interval executions don't "stack up". The next execution is only then scheduled when the previous one finishes. In the case that the execution took longer than the delay, the next execution will be at the next possible time, i.e. ~1ms.

What exactly do you mean with "adjusting the delay on the next one accordingly"? Are you suggesting I should keep track of how long my callback takes to execute and then aim for 60, 30 or 20 fps? How would I know the display's refresh rate anyway? Why wouldn't I schedule the next frame in 1ms, if this one took longer than 16ms?

Trying to correct these timing issues yourself is futile. That's why with Impact (my game engine) I didn't bother with setTimeout and used setInterval instead. Note that requestAnimationFrame aims to fix some of these issues.

My point is, if we would have something like my proposed setAnimation built-in, the browser could do some more optimizations. It could keep track of how long a callback takes to execute on average, match this with the display's refresh rate and schedule garbage collection in between, for instance. I.e. if the browser knows that it has to deal with a long running animation, it could do much more planning than if we just request one frame after the other.

#12 – david – Monday, May 7th 2012, 20:06

I think you need to change the line with the if from

 !anims[current] 
to something like
anims[current] == null 
because it fails on the first run (because the first ID that gets returned is 0, which is falsy)

#13Dominic – Monday, May 7th 2012, 20:17

The spec says that rAF returns a non-zero integer. But I just realized that I don't need this return value anyway. Fixed my article and the gist. Thanks :)

#14Joe Lambert – Tuesday, May 8th 2012, 08:45

I had this prob as well. I've created some drop in replacements for setTimeout and setInterval using rAF: blog.joelambert.co.uk/2011/06/01/a-better-settimeoutsetinterval/

#15 – Boris Zbarsky – Friday, August 10th 2012, 04:07

For what it's worth, it was designed this way on purpose; both options were considered. This way the failure mode when someone screws up is that the animation stops, as opposed to it going on indefinitely and sucking up CPU in the process. This last would be especially bad if a page starts lots of minor animations, as is common: forgetting to stop them would quickly suck up all processing resources. And yes, people make this mistake with setInterval all the time.

So the consensus was that for _users_ this setup is better, because web developers are more likely to notice when they screw up continuing an animation than they are to notice when they screw up stopping it.

#16D. S. Schneider – Wednesday, November 13th 2013, 01:35

@Enrique is right. While setInterval might not get called more than once before the last call was completed, it will indeed stack up many close calls, something you don't want in animation, because many frames will be skipped at once when they finally flush away, unless you are using delta time.

Fast CPUs might get along just fine with setInterval but setTimeout is indeed the right choice for animation, specially on games.

The current API is right the way it is because:

- it allows implementing fixed frame rates (when combined with setTimeout);
- you prevent unnecessary (not to mention annoying) calls before the last one has completed.

Trust me, coupling setTimeout with requestAnimationFrame is the perfect solution for most fixed-rate 2D animations where delta times and floating points would be just overkill. After all, no one has half a pixel on their screen, right?

Post a Comment:

Comment: (Required)

(use <code> tags for preformatted text; URLs are recognized automatically)

Name: (Required)

URL:

Please type phoboslab into the following input field or enable Javascript. This is an anti-spam measure. Sorry for the inconvenience.