Taming requestAnimationFrame and requestIdleCallback

When Paul Irish wrote requestAnimationFrame Scheduling For Nerds I’ve decided to create a tiny utility called Scheduled on Browser and this post goal is to quickly explain why that would most likely benefit your current project too.

Duplicated schedules are useless

I’ve used requestAnimationFrame since its early prefixed days and in various projects, I can grant you it has been often used in the wild for tasks that don’t strictly belong to its use-cases:

All these cases have something in common: these could be executed multiple times because there’s no shared knowledge about who scheduled what in most common applications.

var increment = 0;
function counter() {
  // the internal recursion
  requestAnimationFrame(counter);
  // the operation
  document.body.textContent = ++increment;
}

// somewhere ...
counter();

// somewhere else, later on ...
setTimeout(counter, 1000);

// further on ...
setTimeout(counter, 2000);

If you try above code on any page you like you’ll see that the counter will double its speed after a second, and triplicate after another one.

Moreover, if you check your CPU usage you’ll see that it will quickly increase after those 2 seconds, even if the current task is that trivial.

Using the sob library you can instantly spot the difference.

var increment = 0;
function counter() {
  // the internal recursion
  sob.frame(counter); // or sob.raf(counter);
  // the operation
  document.body.textContent = ++increment;
}

counter();
setTimeout(counter, 1000);
setTimeout(counter, 2000);

There won’t be any duplicated invoke and the CPU will have plenty of room for any other task we’d like to include within that frame.

Why duplicated shouldn’t happen

We don’t know the frame execution stack, all we know is that two identical functions will be triggered twice in the same frame and this is always undesired unless we really need two function calls but honestly name one use case ‘cause I cannot imagine a single one.

Extra arguments to the rescue!

Differently from setTimeout, setInterval, and setImmediate, the requestAnimationFrame as well as the requestIdleCallback don’t come with extra-arguments ability.

var log = console.log.bind(console);
setImmediate(log, 'Hello', 'Immediate'); // Hello Immediate
setTimeout(log, 1000, 'Hello', 'Timeout'); // Hello Tiemout
requestAnimationFrame(log,
  "Hello, it's me",
  "I was wondering if after all these years",
  "you'd like to meet async arguments"
); // 642572.5490000005

The argument passed to requestAnimationFrame is probably one of the least used while extra arguments are one of the most convenient way to avoid creating superfluous user-land callbacks to trap some value. Accordingly, the sob module enable throttling considering also scheduled arguments so that two different calls with different inputs will be scheduled even if based on the same callback.

var log = console.log.bind(console);
sob.frame(log, 'Hello', 'frame');
sob.frame(log, 'Hello there!');
sob.frame(log, 'Hello', 'frame');
// Hello frame
// Hello there!

Above code will schedule only two operations, while he equvalent operation using requestAnimationFrame would look like the following one, although it will trigger twice the same output.

var log = console.log.bind(console);
requestAnimationFrame(function () { log('Hello', 'frame'); });
requestAnimationFrame(function () { log('Hello there!'); });
requestAnimationFrame(function () { log('Hello', 'frame'); });
// Hello frame
// Hello there!
// Hello frame

This is verbose, tedious, and really not so convenient.

Same goes for requestIdleCallback

Everything just said about requestAnimationFrame is exactly the same for requestIdleCallback. We could use all previous examples and use sob.idle(...) or sob.ric(...) instead and observe similar results.

var log = console.log.bind(console);

sob.idle(log, 'Hello', 'frame');
sob.idle(log, 'Hello there!');
sob.idle(log, 'Hello', 'frame');

// VS

requestIdleCallback(function () { log('Hello', 'frame'); });
requestIdleCallback(function () { log('Hello there!'); });
requestIdleCallback(function () { log('Hello', 'frame'); });

It must be noted that using sob.idle instead of requestIdleCallback we lose the ability to check the deadline argument object. It also intended to be like that because the library wants to grant best performance by default so that it takes care of all the logic which aim is to never invoke a scheduled task unless there is some time to do it.

In few words, you should only focus on your logic, and let the helper do its job.

Clearing a scheduled job

The logis is similar to the one found in clearTimeout or clearInterval, where both are actually exchangeable so, to keep it simple, sob.clear(rafOrRicId) would do. Remember, every call to sob.raf(...) or sob.rick(...) will return a unique identifier for that operation.

Such identifier is a plain object you could also use to store any sort of extra info and also, in case you are scheduling twice the same job, the returned id is the same that was scheduled before.

var log = console.log.bind(console);

var id1 = sob.idle(log, 'Hello', 'frame');
var id2 = sob.idle(log, 'Hello', 'frame');
var id3 = sob.idle(log, 'Hello there!');

id1 === id2; // true
id2 === id3; // false

Extra configurations

If you set sob.debug = true the module will console.warn('frame overloaded') whenever the execution stack exceeded the available time per each frame.

Such time is specified by the property minFPS which is 60 frames per second by default. If you are targeting older machines and browsers, I strongly suggest to set sob.minFPS = 30 or, in extreme cases, even 20. This will ensure that there will be more time for tasks that would take more milliseconds to be executed on slower hardware.

Last, but not least, there is a sob.maxIdle property which default is 2000 milliseconds. This means that after a maximum of 2 seconds the idle queue will start triggering, trying to not overload the frame.

As summary

Implementing throttling, extra arguments, and max FPS to grant a smooth user experience is something sooner or later we’ll end up doing. Even if the logic seems to be an overhead, it’s internal operations can be performed in an order of thousands calls per seconds, avoiding concretely any practical slow-down, granting on the other hand best usage of these scheduling based functionalities.

If all you need on your code is a single requestAnimationFrame to the same callback performed in such way:

(function app() {
  requestAnimationFrame(app);
  // the rest of the logic to perform once per frame
}());

then I would not recommend the usage of sob ‘cause there’s not much it could do if not reprioritizing when a frame takes longer. However, in every other slightly more complex scenario, I invite you to give it a try, it’s on npm too.

Last pro-tip about requestIdleCallback

Do not ever do the following:

(function idle() {
  requestIdleCallback(idle);
  /* rest of the logic */
}());

if you need idle tasks, you should rather schedule an interval every N seconds that will invoke requestIdleCallback once executed, otherwise your CPU wil be more busy instead of less.

However, if you compare performance of native requestIdleCallback VS the following:

(function idle() {
  sob.idle(idle);
  /* rest of the logic */
}());

you’ll notice that sob here will help reducing frame overloads, keeping the CPU usage much lower.

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor