Idle Map
When you have code to run that’s async, you have a few options. You can work
with a Promise
, schedule something to run at a later time with setTimeout
,
or schedule in coordination with the browser’s render cycle via
requestAnimationFrame
. Each has its own strength, but now there’s a new tool
in our async toolkit: requestIdleCallback
. I want to show off a trick to mix
up promise-based tasks against the new requestIdleCallback
API (we’ll just
call it rIC
).
If you want a primer on rIC
, check out the
Google article
from
Paul Lewis.
You can also get the full API rundown on
MDN
as well as browser support information from
caniuse.
The idea is to run a list of items through a processing function–essentially
like Array#map
–except we want to ensure we intermittently yield control back
to the main thread to remain responsive for user events. We can use rIC
to
schedule each item’s processing and check the IdleDeadline
to see if there is
more time to process another item. If not, we can schedule another idle
callback. We’ll continue this process until every item in the list has been
processed.
function idleMap(iterable, processCallback) {
return new Promise(resolve => {
let results = [];
let iterator = iterable[Symbol.iterator]();
async function processList(idleDeadline) {
do {
let iterResult = iterator.next();
if (iterResult.done) {
return resolve(results);
}
results.push(await processCallback(iterResult.value));
} while (!idleDeadline.didTimeout);
requestIdleCallback(processList);
}
requestIdleCallback(processList);
});
}
This function, idleMap
takes your list (iterable
) and a callback
(processCallback
), and it applies the callback to every item in the list just
like Array#map
. Internally, it uses recursion by defining a closure
(processList
) that it first schedules with an idle callback. Once that
function is invoked by the browser, it uses the iterator to pull out items from
the list and applies the processing callback on them. After each item, the
do..while
control will evaluate whether or not the idle deadline has expired.
If it hasn’t, the function is free to process another item. If the deadline has
expired, the do..while
control breaks and schedules another idle callback to
continue processing the list. Once the list iterator has been consumed, the
promise returned from idleMap
resolves with the results of each item’s
processing.
I find that using the
iterator interface
works well with the do..while
control flow by removing the need to managing
array indexes. As a major bonus, it also means we can map anything that
satisfies the iterable interface. This could be doubly useful since it would
allow the use of generator functions, custom objects, and various other
non-array types to supply the items to be processed.