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:
- invoked just to perform slightly asynchronous code that involves CSS classes changes
- invoked to make something slightly asynchronous but “optimized” more than a
setTimeout(fn, 0)
could be - invoked externally through callbacks that invoke it internally, creating recursive loop able to degrade performance instead of improving them
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.