How to use the Background Tasks API in Javascript

In Javascript it is often difficult to understand when to schedule and to execute lower priority tasks efficiently. This API can help you do that.

When a browser loads a web page, all scripts inside are executed on a single thread according to Event Loop model. Basically it implies that all called or invoked functions are queued and then executed by the Javascript engine, one by one. So the greater the execution time of a function, the greater the delay for the start of the next ones at the risk of affecting the speed and responsiveness of our web page.

Take, for example, a website with an image gallery. When user scrolls the page several tasks can be scheduled and queued: an http request to get images' data, the insertion of new elements in the DOM, the sending of analytics or logs and so on. The greater the number of actions performed by a user on a page, the greater the queue of tasks to be performed. Obviously the Javascript engine don't know the priority of each task and queues them according to the order in which they were called at the risk of not putting in top those with higher priority (e.g. the updating of view). If we also consider scenarios such as navigation on mobile or old devices, we can figure out how important the order and execution time of each task is.

The Background Tasks API (also known as Cooperative Scheduling of Background Tasks API from the name given by W3C or requestIdleCallback() from the function's name) help us in the purpose, indeed it allows us to schedule tasks to be performed when the page is in idle state. In this way we are able to execute before functions with high priority and after those with lower priority.

Use case

Take for example a generic log function and use it at the end of an operation

function log(action) {
  console.log(action + ': done!')
  // Do something like sending analytics...
}

function doSomething(action) {
  console.log(action + ': executing...')
  // Do something...
  log(action)
}

doSomething('operation #1')
doSomething('operation #2')
console.log('another operation')
doSomething('operation #3')

The output will be:

operation #1: executing...
operation #1: done!
operation #2: executing...
operation #2: done!
another operation
operation #3: executing...
operation #3: done!

As you can see, if we call doSomething() several times, we queue each operation inside it, including the execution of log(), according to the order in which they appear. This is not optimal from a perfomance standpoint because, if in doSomething() there are functions which should be executed as soon as possible, such as updating the user interface or performing some animations, it is important that they take precedence over those of secondary importance in order not to make the user's navigation slow or non-fluid.

At first glance it seems that the problem can be solved by removing the log() function from the body of doSomething() and calling it in another place.

doSomething('operation #1')
doSomething('operation #2')
doSomething('operation #3')
log('operation #1')
log('operation #2')
log('operation #3')

But this approach has some downsides. In this way we increase the verbosity of our code (think if instead of strings the above functions accept anonymous functions) and lose the context of the function which we should include in our log.

A workaround used in these type of situations is exploiting setTimeout() to execute a function after those that have already been scheduled.

function log(action) {
  setTimeout(function () {
    console.log(action + ': done!')
    // Do something like sending analytics...
  }, 1)
}

Unfortunately, even in this way the problem can't be completely addressed because in doSomething() there could be asynchronous functions or event handlers. In these cases, in fact, it isn't possible predict when they will be scheduled so we can't be sure to execute the secondary tasks after the main ones.

Syntax

The requestIdleCallback() function accepts two parameters:

  • A callback to be executed while the page is in idle. An object is passed to the callback as parameter with:

    • The didTimeout property. It is a boolean value which tell us if the callback has been executed because the time set by timeout has expired (see below).
    • The timeRemaining() method. It returns the time remaining to the end of the current idle state (maximum 50ms for perfomance reasons). It can be used to perform a list of tasks.
  • An optional object to define the settings. At the moment we can configure only one setting through the timeout property to which we can assign an integer corresponding to the maximum delay (expressed in milliseconds) to execute the callback.

The function returns an id which we can use to cancel the execution of the passed callback by using cancelIdleCallback().

Here a complete example of described functions

var idleCallbackId = requestIdleCallback(
  function (idleDeadline) {
    // True if the callback has been executed because setted 'timeout' has expired.
    var didTimeout = idleDeadline.didTimeout
    // Time remaining to the end of the current idle state.
    var timeRemaining = idleDeadline.timeRemaining()
  },
  {
    // Maximum delay to execute the passed callback. Optional.
    timeout: 3000,
  }
)

// Cancel the callback's execution
cancelIdleCallback(idleCallbackId)

Usage

Let's refactor the log() by using requestIdleCallback()

function log(action) {
  requestIdleCallback(function () {
    console.log(action + ': done!')
    // Do something like sending analytics...
  })
}

If we perform previous operations again we can observe that log(), even if called several times and after other functions, will not be executed before them.

doSomething('operation #1')
doSomething('operation #2')
console.log('another operation')
doSomething('operation #3')
operation #1: executing...
operation #2: executing...
another operation
operation #3: executing...
operation #1: done!
operation #2: done!
operation #3: done!

Design pattern

  • Be careful to use this API to edit the DOM. Remember that requestIdleCallback() is designed to perform background tasks while the page is in indle, then it isn't the best option to update the user interface. Furthermore the DOM manipulation may trigger a reflow of elements in the page and affect performances. A better approach is using a DocumentFragment object, attaching to it the nodes which we want insert in the page and adding it to the DOM using a function to be executed outside of the idle state or an API like requestAnimationFrame()

Let's see how to add a h1 tag to page using this approach

function updatePageTitle(titleElement) {
  if (requestAnimationFrame) {
    requestAnimationFrame(function () {
      document.body.insertBefore(titleElement, document.body.firstChild)
    })
  } else {
    document.body.insertBefore(titleElement, document.body.firstChild)
  }
}

requestIdleCallback(function () {
  var documentFragment = document.createDocumentFragment()
  var title = document.createElement('h1')
  title.textContent = 'My title'
  documentFragment.appendChild(title)
  updatePageTitle(documentFragment)
})
  • You can perform a list of tasks using requestIdleCallback(). To prevent the remaining tasks from being skipped at the end of the idle state, you can use timeRemaining() to schedule again the callback until all task have been performed.
// Tasks to be executed in background
var tasks = [
  function a() {
    console.log('function A')
  },
  function b() {
    console.log('function B')
  },
  function c() {
    console.log('function C')
  },
]

// Main function to perform the background tasks
function executeTasks(idleDeadline) {
  // Until we are in the idle state and there are still tasks in the list, perform them
  while (idleDeadline.timeRemaining() > 0 && tasks.length > 0) {
    // Execute and remove the first task from the list
    tasks.shift()()
  }
  // We are no longer in the idle state. Check if there are still tasks in the list and
  // if so reschedule the main function
  if (tasks.length > 0) {
    requestIdleCallback(executeTasks)
  }
}

requestIdleCallback(executeTasks)

Compatibility

At the time of writing this article, this API is still under development and the browsers that support it are as follows

Chrome Firefox Opera
47 55 34

This shim allows us to use it even in unsupported browsers

window.requestIdleCallback =
  window.requestIdleCallback ||
  function (cb) {
    return setTimeout(function () {
      var start = Date.now()
      cb({
        didTimeout: false,
        timeRemaining: function () {
          return Math.max(0, 50 - (Date.now() - start))
        },
      })
    }, 1)
  }

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

Source: gist.github.com/paullewis/55efe5d6f05434a96c36

Note the use of setTimeout(), which we have seen earlier, to emulate the behaviour of the API as much as possible.