Background Tasks API

您正在阅读此内容的英文版本,因为该语系尚未翻译。 帮助我们翻译此文章吧!

草案
本页尚未完工.

幕后任务协作调度 API (也叫幕后任务 API 或者简单称为 requestIdleCallback() API) 提供了由用户代理决定,在空闲时间自动执行队列任务的能力。

概念和用法

浏览器的主线程以其事件循环队列为中心。此代码渲染 Document 上待更新展示的内容,执行页面待运行的 JavaScript 脚本,接收来自输入设备的事件,以及分发事件给需要接收事件的元素。此外,事件循环队列处理与操作系统的交互、浏览器自身用户界面的更新等等。这是一个非常繁忙的代码块,您的主要 JavaScript 代码可能会和这些代码一起也在这个线程中执行。当然,大多数(不是所有)能够更改 DOM 的代码都在主线程中运行,因为用户界面更改通常只对主线程可用。

因为事件处理和屏幕更新是用户关注性能最明显的两种方式。对于您的代码来说,防止在事件队列中出现卡顿是很重要的。在过去,除了编写尽可能高效的代码和将尽可能多的工作移交给 workers 之外,没有其他可靠的方法可以做到这一点。 Window.requestIdleCallback() 允许浏览器告诉您的代码可以安全使用多少时间而不会导致系统延迟,从而有助于确保浏览器的事件循环平稳运行。如果您保持在给定的范围内,您可以使用户体验更好。

充分利用空闲回调

因为 idle callbacks 旨在为代码提供一种与事件循环协作的方式,以确保系统充分利用其潜能,不会过度分配任务,从而导致延迟或其他性能问题,因此您应该考虑如何使用它。

  • 对非高优先级的任务使用空闲回调。 已经创建了多少回调,用户系统的繁忙程度,你的回调多久会执行一次(除非你指定了 timeout),这些都是未知的。不能保证每次事件循环(甚至每次屏幕更新)后都能执行空闲回调;如果时间循环用尽了所有可用时间,那你可就倒霉了(再说一遍,除非你用了 timeout)。 
  • 空闲回调应尽可能不超支分配到的时间。尽管即使你超出了规定的时间上限,通常来说浏览器、代码、网页也能继续正常运行,这里的时间限制是用来保证系统能留有足够的时间去完成当前的事件循环然后进入下一个循环,而不会导致其他代码卡顿或动画效果延迟。目前,timeRemaining() 有一个50 ms 的上限时间,但实际上你能用的时间比这个少,因为在复杂的页面中事件循环可能已经花费了其中的一部分,浏览器的扩展插件也需要处理时间,等等。
  • 避免在空闲回调中改变 DOM。 空闲回调执行的时候,当前帧已经结束绘制了,所有布局的更新和计算也已经完成。If you make changes that affect layout, you may force a situation in which the browser has to stop and do recalculations that would otherwise be unnecessary. If your callback needs to change the DOM, it should use Window.requestAnimationFrame() to schedule that.
  • Avoid tasks whose run time can't be predicted. Your idle callback should avoid doing anything that could take an unpredictable amount of time. For example, anything which might affect layout should be avoided. You should also avoid resolving or rejecting Promises, since that would invoke the handler for that promise's resolution or rejection as soon as your callback returns.
  • Use timeouts when you need to, but only when you need to. Using timeouts can ensure that your code runs in a timely manner, but it can also allow you to cause lag or animation stutters by mandating that the browser call you when there's not enough time left for you to run without disrupting performance.

Falling back to setTimeout

Because the Background Tasks API is fairly new, your code may need to be able to work on browsers that don't yet support it. You can do so with a simple shim that uses setTimeout() as a fallback option. This isn't a polyfill, since it's not functionally identical; setTimeout() doesn't let you make use of idle periods, but instead runs your code when possible, leaving us to do the best we can to avoid causing the user to experience performance lag.

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);
}

If window.requestIdleCallback is undefined, we create it here. The function begins by recording the time at which our implementation was called. We'll be using that to compute the value returned by our shim for timeRemaining().

Then we call setTimeout(), passing into it a function which runs the callback passed into our implementation of requestIdleCallback(). The callback is passed an object which conforms to IdleDeadline, with didTimeout set to false and a timeRemaining() method which is implemented to give the callback 50 milliseconds of time to begin with. Each time timeRemaining() is called, it subtracts the elapsed time from the original 50 milliseconds to determine the amount of time left.

As a result, while our shim doesn't constrain itself to the amount of idle time left in the current event loop pass like the true requestIdleCallback(), it does at least limit the callback to no more than 50 milliseconds of run time per pass.

The implementation of our shim for cancelIdleCallback() is much simpler:

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

If cancelIdleCallback() isn't defined, this creates one which simply passes the specified callback ID through to clearTimeout().

Now your code will work even on browsers that don't support the Background Tasks API, albeit not as efficiently.

接口

The Background Tasks API adds only one new interface:

IdleDeadline
An object of this type is passed to the idle callback to provide an estimate of how long the idle period is expected to last, as well as whether or not the callback is running because its timeout period has expired.

The Window interface is also augmented by this API to offer the new requestIdleCallback() and cancelIdleCallback() methods.

示例

In this example, we'll take a look at how you can use requestIdleCallback() to run time-consuming, low-priority tasks during time the browser would otherwise be idle. In addition, this example demonstrates how to schedule updates to the document content using requestAnimationFrame().

Below you'll find only the HTML and JavaScript for this example. The CSS is not shown, since it's not particularly crucial to understanding this functionality.

HTML content

In order to be oriented about what we're trying to accomplish, let's have a look at the HTML. This establishes a box (ID "Container") that's used to present the progress of an operation (because you never know how long decoding "quantum filament tachyon emissions" will take, after all) as well as a second main box (with the ID "logBox"), which is used to display textual output.

<p>
  Demonstration of using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API">
  cooperatively scheduled background tasks</a> using the <code>requestIdleCallback()</code>
  method.
</p>

<div class="container">
  <div class="label">Decoding quantum filament tachyon emissions...</div>
  <progress id="progress" value="0"></progress>
  <div class="button" id="startButton">
    Start
  </div>
  <div class="label counter">
    Task <span id="currentTaskNumber">0</span> of <span id="totalTaskCount">0</span>
  </div>
</div>

<div class="logBox">
  <div class="logHeader">
    Log
  </div>
  <div id="log">
  </div>
</div>

The progress box uses a <progress> element to show the progress, along with a label with sections that are changed to present numeric information about the progress. In addition, there's a "Start" button (creatively given the ID "startButton"), which the user will use to start the data processing.

JavaScript content

Now that the document structure is defined, construct the JavaScript code that will do the work. The goal: to be able to add requests to call functions to a queue, with an idle callback that runs those functions whenever the system is idle for long enough a time to make progress.

Variable declarations

let taskList = [];
let totalTaskCount = 0;
let currentTaskNumber = 0;
let taskHandle = null;

These variables are used to manage the list of tasks that are waiting to be performed, as well as status information about the task queue and its execution:

  • taskList is an Array of objects, each representing one task waiting to be run.
  • totalTaskCount is a counter of the number of tasks that have been added to the queue; it will only go up, never down. We use this to do the math to present progress as a precentage of total work to do.
  • currentTaskNumber is used to track how many tasks have been processed so far.
  • taskHandle is a reference to the task currently being processed.
let totalTaskCountElem = document.getElementById("totalTaskCount");
let currentTaskNumberElem = document.getElementById("currentTaskNumber");
let progressBarElem = document.getElementById("progress");
let startButtonElem = document.getElementById("startButton");
let logElem = document.getElementById("log");

Next we have variables which reference the DOM elements we need to interact with. These elements are:

  • totalTaskCountElem is the <span> we use to insert the total number of tasks created into the status display in the progress box.
  • currentTaskNumberElem is the element used to display the number of tasks processed so far.
  • progressBarElem is the <progress> element showing the percentage of the tasks processed so far.
  • startButtonElem is the start button.
  • logElem is the <div> we'll insert logged text messages into.
let logFragment = null;
let statusRefreshScheduled = false;

Finally, we set up a couple of variables for other items:

  • logFragment will be used to store a DocumentFragment that's generated by our logging functions to  create content to append to the log when the next animation frame is rendered.
  • statusRefreshScheduled is used to track whether or not we've already scheduled an update of the status display box for the upcoming frame, so that we only do it once per frame

Managing the task queue

Next, let's look at the way we manage the tasks that need to be performed. We're going to do this by creating a FIFO queue of tasks, which we'll run as time allows during the idle callback period.

Enqueueing tasks

First, we need a function that enqueues tasks for future execution. That function, enqueueTask(), looks like this:

function enqueueTask(taskHandler, taskData) {
  taskList.push({
    handler: taskHandler,
    data: taskData
  });
 
  totalTaskCount++;
 
  if (!taskHandle) {
    taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
  }
 
  scheduleStatusRefresh();
}

enqueueTask() accepts as input two parameters:

  • taskHandler is a function which will be called to handle the task.
  • taskData is an object which is passed into the task handler as an input parameter, to allow the task to receive custom data.

To enqueue the task, we push an object onto the taskList array; the object contains the taskHandler and taskData values under the names handler and data, respectively, then increment totalTaskCount, which reflects the total number of tasks which have ever been enqueued (we don't decrement it when tasks are removed from the queue).

Next, we check to see if we already have an idle callback created; if taskHandle is 0, we know there isn't an idle callback yet, so we call requestIdleCallback() to create one. It's configured to call a function called runTaskQueue(), which we'll look at shortly, and with a timeout of 1 second, so that it will be run at least once per second even if there isn't any actual idle time available.

Running tasks

Our idle callback handler, runTaskQueue(), gets called when the browser determines there's enough idle time available to let us do some work or our timeout of one second expires. This function's job is to run our enqueued tasks.

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;
  }
}

runTaskQueue()'s core is a loop which continues as long as there's time left (as determined by checking IdleDeadline.timeRemaining) to be sure it's more than 0 or if the timeout limit was reached (deadline.didTimeout is true), and as long as there are tasks in the task list.

For each task in the queue that we have time to execute, we do the following:

  1. We remove the task object from the queue.
  2. We increment currentTaskNumber to track how many tasks we've executed.
  3. We call the task's handler, task.handler, passing into it the task's data object (task.data).
  4. We call a function, scheduleStatusRefresh(), to handle scheduling a screen update to reflect changes to our progress.

When time runs out, if there are still tasks left in the list, we call requestIdleCallback() again so that we can continue to process the tasks the next time there's idle time available. If the queue is empty, we set taskHandle to 0 to indicate that we don't have a callback scheduled. That way, we'll know to request a callback next time enqueueTask() is called.

Updating the status display

One thing we want to be able to do is update our document with log output and progress information. However, you can't safely change the DOM from within an idle callback. Instead, we'll use requestAnimationFrame() to ask the browser to call us when it's safe to update the display.

Scheduling display updates

DOM changes are scheduled by calling the scheduleStatusRefresh() function.

function scheduleStatusRefresh() {
    if (!statusRefreshScheduled) {
      requestAnimationFrame(updateDisplay);
      statusRefreshScheduled = true;
  }
}

This is a simple function. It checks to see if we've already scheduled a display refresh by checking the value of statusRefreshScheduled. If it's false, we call requestAnimationFrame() to schedule a refresh, providing the updateDisplay() function to be called to handle that work.

Updating the display

The updateDisplay() function is responsible for drawing the contents of the progress box and the log. It's called by the browser when the DOM is in a safe condition for us to apply changes during the process of rendering the next frame.

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;
}

First, scrolledToEnd is set to true if the text in the log is scrolled to the bottom; otherwise it's set to false. We'll use that to determine if we should update the scroll position to ensure that the log stays at the end when we're done adding content to it.

Next, we update the progress and status information if any tasks have been enqueued.

  1. If the current maximum value of the progress bar is different from the current total number of enqueued tasks (totalTaskCount), then we update the contents of the displayed total number of tasks (totalTaskCountElem) and the maximum value of the progress bar, so that it scales properly.
  2. We do the same thing with the number of tasks processed so far; if progressBarElem.value is different from the task number currently being processed (currentTaskNumber), then we update the displayed value of the currently-being-processed task and the current value of the progress bar.

Then, if there's text waiting to be added to the log (that is, if logFragment isn't null), we append it to the log element using Element.appendChild() and set logFragment to null so we don't add it again.

If the log was scrolled to the end when we started, we make sure it still is. Then we set statusRefreshScheduled to false to indicate that we've handled the refresh and that it's safe to request a new one.

Adding text to the log

The log() function adds the specified text to the log. Since we don't know at the time log() is called whether or not it's safe to immediately touch the DOM, we will cache the log text until it's safe to update. Above, in the code for updateDisplay(), you can find the code that actually adds the logged text to the log element when the animation frame is being updated.

function log(text) {
  if (!logFragment) {
      logFragment = document.createDocumentFragment();
  }
 
  let el = document.createElement("div");
  el.innerHTML = text;
  logFragment.appendChild(el);
}

First, we create a DocumentFragment object named logFragment if one doesn't currently exist. This element is a pseudo-DOM into which we can insert elements without immediately changing the main DOM itself.

We then create a new <div> element and set its contents to match the input text. Then we append the new element to the end of the pseudo-DOM in logFragment. logFragment will accumulate log entries until the next time updateDisplay() is called because the DOM for the changes.

Running tasks

Now that we've got the task management and display maintenance code done, we can actually start setting up code to run tasks that get work done.

The task handler

The function we'll be using as our task handler—that is, the function that will be used as the value of the task object's handler property—is logTaskHandler(). It's a simple function that outputs a bunch of stuff to the log for each task. In your own application, you'd replace this code with whatever task it is you wish to perform during idle time. Just remember that anything you want to do that changes the DOM needs to be handled through requestAnimationFrame().

function logTaskHandler(data) {
  log("<strong>Running task #" + currentTaskNumber + "</strong>");
 
  for (i=0; i<data.count; i+=1) {
    log((i+1).toString() + ". " + data.text);
  }
}

The main program

Everything is triggered when the user clicks the Start button, which causes the decodeTechnoStuff() function to be called.

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);

decodeTechnoStuff() starts by zeroing the values of totalTaskCount (the number of tasks added to the queue so far) and currentTaskNumber (the task currently being run), and then calls updateDisplay() to reset the display to its "nothing's happened yet" state.

This example will create a random number of tasks (between 100 and 200 of them). To do so, we use the getRandomIntInclusive() function that's provided as an example in the documentation for Math.random() to get the number of tasks to create.

Then we start a loop to create the actual tasks. For each task, we create an object, taskData, which includes two properties:

  • count is the number of strings to output into the log from the task.
  • text is the text to output to the log the number of times specified by count.

Each task is then enqueued by calling enqueueTask(), passing in logTaskHandler() as the handler function and the taskData object as the object to pass into the function when it's called.

Result

Below is the actual functioning result of the code above. Try it out, play with it in your browser's developer tools, and experiment with using it in your own code.

规范

Specification Status Comment
Cooperative Scheduling of Background Tasks Proposed Recommendation

浏览器兼容性

Update compatibility data on GitHub
DesktopMobile
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome for AndroidFirefox for AndroidOpera for AndroidSafari on iOSSamsung Internet
requestIdleCallback
Experimental
Chrome Full support 47Edge No support NoFirefox Full support 55
Notes
Full support 55
Notes
Notes Enabled by default.
No support 53 — 55
Notes
Notes Implemented but disabled by default.
IE No support NoOpera Full support 34Safari No support NoWebView Android Full support 47Chrome Android Full support 47Firefox Android Full support 55
Notes
Full support 55
Notes
Notes Enabled by default.
No support 53 — 55
Notes
Notes Implemented but disabled by default.
Opera Android Full support YesSafari iOS No support NoSamsung Internet Android Full support Yes

Legend

Full support  
Full support
No support  
No support
Experimental. Expect behavior to change in the future.
Experimental. Expect behavior to change in the future.
See implementation notes.
See implementation notes.

相关链接