Running more code from a requested animation frame in the same frame


#1

We can use requestAnimationFrame to request a frame between browser paints in which to run code. Then, if we call requestAnimationFrame from that code, it schedules the code in the new call to run at the next frame.

Is there some way to tell the browser “Hey, if there’s still time in this frame, run this new code in this same frame, not the next frame, but if there isn’t time, run it in the next frame.”?

Is this a good place to ask? Or maybe SO is better? Any other places I might try?


#2

You may want to look into requestIdleCallback.


#3

The idle callbacks have no defined time to run; in a busy period they might not run at all for several frames until the browser finally calms down. This fails both of the requests: there’s no guarantee that this’ll happen before frame commit, even if there’s time; and there’s no guarantee it’ll run in the next rAF period either.

There’s no way to request this right now; as you’ve found, any rAF callbacks added during the rAF period are put in the next frame, as that’s usually what you want - most of the time you’ll be doing work per frame, and running twice in the same frame would be silly.

Can you elaborate on what you’re trying to do? What work are you trying to make async that doesn’t care whether it runs in the current rAF or the next? It might be that you’re not actually wanting a rAF at all, but are indeed asking for an idleCallback, and rAF timing just happens to be the closest you have to that at the moment.


#4

If you have 2 separate rAF() callbacks, then my understanding is that the browser may execute them both in the same frame if there is time.

For example:

requestAnimationFrame(() => { /* something quick */ });
requestAnimationFrame(() => { /* something also quick */ });

Assuming this works, you might be able to exploit this. For example:

let doSecondTask = false;
requestAnimationFrame(() => {
  doSecondThing = false; // disable the second task at the start of each frame
  /* something quick */
  doSecondThing = true; // only do this when we'd like the second task
});
requestAnimationFrame(() => {
  if (doSecondTask) {
    /* something also quick */
  }
});

Without thorough testing, this is probably a bad thing to recommend. /shrug


#5

Ooh, clever (but terribly hacky). It’ll definitely work, though - you’re correct that multiple rAF callbacks will be executed in the same frame if there’s time. It’s just terrible. :slight_smile:


#6

Thanks for the input guys. I didn’t know about requestIdleCallback until now. It has an interesting feature where it lets you detect time remaining until the end of the rAF (if we’re lucky enough for it to fire):

requestIdleCallback(function myNonEssentialWork (deadline) {
  while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
})

It seems deadline.timeRemaining() shows the time remaining until the end of the rAF.

I’m trying to determine the best possible way to handle the animation loop for the engine we’re working on at infamous.io (name subject to change).

Maybe I can request a single animation frame callback, then place all rendering code into idle callbacks so I always know how much time is left in the frame. I’m also using raf-timeout to see if we can prevent 3rd party libs from running async code at times that might effect rendering. I haven’t experimented with monkey patching Promise.resolve().then yet, which is faster than setTimeout(..., 0). In my apps, I never need (yet) to execute anything in a future-but-soon-as-possible tick, so monkey patching setTimeout, etc, with rAF is fine in my cases, and probably fine for the vast majority of apps.

@tabatkins @yoavweiss @jokeyrhyme One reason I’m researching this is I’m thinking that there might be a way to specify some animations in the engine to be secondary, so that they can run at lower FPS when necessary in order for primary animations to be at 60fps; if we know there’s no time remaining in the frame, then we can skip some frames of the secondary animation. Another thing I’m trying to achieve is figuring out if we’re in a frame or not when calling the setter of a Node in the scene graph: calling node.rotation = [0,0,20], for example, will trigger the engine to request an animation frame in order to call node.render(), but, if we’re already in a frame, we don’t need to queue more rAF’s when the node.rotation setter is used inside the rAF, so I’m determining how to handle that.


#8

It seems I need something that is guaranteed to fire at the end of a frame even if the user happens to be interacting (because the engine is intended for making interactive things). Maybe passing {timeout:0} can guarantee that the idle callback will fire after each rAF: requestIdleCallback(..., {timeout:0})? Because, I still need it to fire no matter what, and if timeout:0 does the trick, then the deadline argument passed into the callback will tell me if I exceeded the frame time or not. I’ll experiment with it…