requestIdleCallback()

The Background Tasks API provides the ability to queue tasks to be executed automatically when the user agent determines that there is free time to do so.

Because requestIdleCallback() has not been supported by browsers such as Safari, you may wish to create the polyfill functions:
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);}
This briefly illustrates how to queue tasks for execution during free time.
let taskList = [];
let totalTaskCount = 0;
let currentTaskNumber = 0;
let taskHandle = null;
let logFragment = null;
let statusRefreshScheduled = false;
function enqueueTask(taskHandler, taskData) {
  taskList.push({
    handler: taskHandler,
    data: taskData
  });
  totalTaskCount++;
  if (!taskHandle)taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
  scheduleStatusRefresh();
}
function runTaskQueue(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskList.length) {
    let task = taskList.shift();
    currentTaskNumber++;
    task.handler(task.data);
    scheduleStatusRefresh();
  }
  if (taskList.length) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000} );
  } else {
    taskHandle = 0;
  }
}
function scheduleStatusRefresh() {
   if (!statusRefreshScheduled) {
      requestAnimationFrame(updateDisplay);
      statusRefreshScheduled = true;
   }
}
function updateDisplay() {
   let scrolledToEnd = logElem.scrollHeight - logElem.clientHeight <= logElem.scrollTop + 1;
   if (totalTaskCount) {
      if (progressBarElem.max != totalTaskCount) {
         totalTaskCountElem.textContent = totalTaskCount;
         progressBarElem.max = totalTaskCount;
      }
      if (progressBarElem.value != currentTaskNumber) {
         currentTaskNumberElem.textContent = currentTaskNumber;
         progressBarElem.value = currentTaskNumber;
      }
   }
   if (logFragment) {
      logElem.appendChild(logFragment);
      logFragment = null;
   }
   if (scrolledToEnd) {
      logElem.scrollTop = logElem.scrollHeight – logElem.clientHeight;
   }
   statusRefreshScheduled = false;
}
function logTaskHandler(data) {
   log("<strong>Running task #" + currentTaskNumber + "</strong>");
   for (i=0; i<data.count; i+=1) {
      log((i+1).toString() + ". " + data.text);
   }
}
function decodeTechnoStuff() {
   totalTaskCount = 0;
   currentTaskNumber = 0;
   updateDisplay();
   let n = getRandomIntInclusive(100, 200);
   for (i=0; i<n; i++) {
      let taskData = {
         count: getRandomIntInclusive(75, 150),
         text: "This text is from task number " + (i+1).toString() + " of " + n
      };
      enqueueTask(logTaskHandler, taskData);
  }
}
document.getElementById("startButton")
        .addEventListener("click", decodeTechnoStuff, false);