When executing scripts on a page, one thing we want to avoid is creating unnecessary jank for the user. This particularly true when we’re doing a lower priority task that doesn’t have to be called that exact moment. In those cases, it would be helpful to be able to delay those calls until we’re sure the browser isn’t working on something more important.

With requestIdleCallback(), an API available in many current browsers, we can do just that: delay execution of scripts until the browser has enough time to run them smoothly. From the MDN docs:

“Window.requestIdleCallback() makes it possible to become actively engaged in helping to ensure that the browser’s event loop runs smoothly, by allowing the browser to tell your code how much time it can safely use without causing the system to lag…” (Source)

This effectively allows us to register a callback function to be executed when the browser is idle. Instead of us trying to calculate when to run certain tasks, we can simply tell the browser to run them whenever it gets a chance. That way, they’ll get done, but they won’t get called at the exact moment the browser is busy handling some other user interaction or event.

Usage and Syntax

Scheduling the Callback

The syntax to schedule a callback during an idle period is:

requestIdleCallback(callback[, options]);

The first argument is the callback function we want executed when the browser is idle next. The second argument is an object we can pass in additional options with. Current the only option for this object is setting a timeout. This allows us to specify the maximum amount of time we’re willing to wait before the browser must execute the callback.

For instance, if I wanted to call a function that will process some lower priority tasks, but I also want to ensure that it gets run within the next 3 seconds, I could do this:

requestIdleCallback(doLowPriorityTask, { timeout: 3000 });

This would add a call to my doLowPriorityTask() function into the queue of things to do whenever the browser is idle. But if that doesn’t happen in the next 3 seconds, the function would be called regardless.

Note: Calling requestIdleCallback() will return an ID which can be used to later cancel the callback.

Canceling the Callback

In those situations where you need to cancel a previously scheduled callback, you can do so by using:

cancelIdleCallback(handle);

The handle is the ID value returned when initially scheduling the callback.

The Callback Itself

When the callback is called, it will be passed an IdleDeadline object. This object contains a didTimeout property and a timeRemaining() method.

  • didTimeout: A Boolean that tells us whether the callback was executed because it hit the specified timeout.

  • timeRemaining(): Provide an estimate of the number of milliseconds remaining in the current idle period. If it’s over, the value will be 0.

This allows us to check how much time is left, or whether we hit a timeout. With this information, we can decide how we want to proceed within the callback. For some tasks, we may want to ensure that we have a certain minimum amount of time remaining in the idle period. Or we may want to handle a task differently if it’s hit the timeout.

  // Do tasks if there's any time remaining,
  // or if the timeout has been hit

  function doLowPriorityTask(deadline) {
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) {
      // Do task(s)
    }
  }

Support

requestIdleCallback() is currently supported by Chrome, Firefox, Opera, and Android. MS Edge and Safari are still under consideration. For the browsers that do not support it, there is a standard fallback that can be used:

// requestIdleCallback() Fallback

window.requestIdleCallback = window.requestIdleCallback || function(handler) {
  let startTime = Date.now();
  return setTimeout(function() {
    handler({
      didTimeout: false,
      timeRemaining: function() {
        return Math.max(0, 50.0 - (Date.now() - startTime));
      }
    });
  }, 1);
}

window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
  clearTimeout(id);
}

Best Practices

When scheduling tasks using requestIdleCallback(), there are a few best practices to keep in mind.

  1. Use it for lower priority tasks. This may be obvious, but if you choose to wait until the system’s idle to execute a task, you’re losing control of when that task will be called. Which means you won’t want to do this for critical tasks that need to be done immediately.

    But if it’s something that can afford a little delay, like sending various analytics information, allowing the browser to fit it in whenever it gets a moment makes sense.

  2. Refrain from changing the DOM. Since the idle time will come at the end of the frame, all the layout changes will have already been made by the time the callback is called. Making further changes to the DOM will force the browser to stop and redo its layout calculations. If there are changes to the DOM, it’s better to use requestAnimationFrame() to make those changes.

    One pattern to do this is to use a documentFragment (document.createDocumentFragment()) to store pending changes made in the scheduled task, and then use requestAnimationFrame() to add this fragment to the DOM.

  3. Stay away from tasks which could take an unpredictable amount of time. A good example would be resolving or rejecting Promises. Doing so would invoke the handler for that promise’s resolution or rejection as soon as your callback returns, which may or may not be a good time for it to be invoked.

  4. Use timeouts only when needed. Timeouts allow you to ensure the task gets run at a specific time, but depending on what else the browser is trying to do at that moment, this may also cause some performance issues if there are lots of other things going on.

Example

Mozilla has a good example that shows how all these various pieces can work together: https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API#Example

Resources