Close up of a map.

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.