使用 requestIdleCallback

使用 requestIdleCallback

许多网站和应用程序都有很多脚本来执行。您的 JavaScript 通常需要尽快运行,但同时您不希望它阻碍用户那条路。如果您在用户滚动页面时发送分析数据,或者在轻触按钮时将元素附加到 DOM,则您的 Web 应用程序将无法响应,从而导致用户体验不佳。

https://developers.google.com/web/updates/images/2015-08-27-using-requestidlecallback/main.png

好消息是,现在有一个 API 可以帮助:requestIdleCallback。以同样的方式,requestAnimationFrame 我们可以正确地安排动画,并最大限度地提高 60fps 的机会,requestIdleCallback 当一个框架结束时,或当用户处于非活动状态时,可以安排工作。这意味着有机会在用户不使用的情况下进行工作。它可用于 Chrome 47!这是一个实验性的功能,规格还在不断变化,所以将来会发生变化。

检查 requestIdleCallback 支持

这是早期的 requestIdleCallback,所以在使用它之前,您应该检查它是否可用:

if ('requestIdleCallback' in window) {
  // Use requestIdleCallback to schedule work.
} else {
  // Do what you’d do today.
}

你也可以 polyfill 它的行为,这需要回到 setTimeout

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

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

使用 setTimeout 不是很好,因为它不知道 requestIdleCallback 的空闲时间是如何工作的,但是如果 requestIdleCallback 不可用的话,你可以直接调用你的回调,你没有比这更差的方式。有了垫片,requestIdleCallback 就可以使用,你将被默认重定向到直接调用,这是很棒的。

现在,我们假设它存在。

使用requestIdleCallback

调用 requestIdleCallback 非常类似于 requestAnimationFrame, 它需要回调函数作为其第一个参数:

requestIdleCallback(myNonEssentialWork);

myNonEssentialWork 被调用时,它将被赋予一个 deadline, 它是一个包含函数的对象,该函数返回一个数字,指示您的工作剩余多少时间:

function myNonEssentialWork (deadline) {
  while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

timeRemaining 可以调用该函数来获取最新值。当 timeRemaining() 返回零时,您可以安排另一个 requestIdleCallback, 如果您还有更多的工作要做:

function myNonEssentialWork (deadline) {
  while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

  if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

调用你的任务

如果浏览器真的很忙,你该怎么办?您可能会担心您的回调可能永远不会被调用。嗯,虽然 requestIdleCallback 类似 requestAnimationFrame,但它也有所不同,它需要一个可选的第二个参数:具有 timeout 属性的 options 对象。此超时(如果设置)为浏览器提供了必须执行回调的时间(以毫秒为单位):

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

如果您的回调由于超时触发而执行,您会注意到两件事情:

  • timeRemaining() 将返回零。
  • 对象的 didTimeout 属性 deadline 将为 true

如果你看到这 didTimeout 是真的,你很可能只想运行这个工作并完成它:

function myNonEssentialWork (deadline) {

  // Use any remaining time, or, if timed out, just run through the tasks.
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0)
    doWorkIfNeeded();

  if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

由于潜在的中断,这种超时可能会导致您的用户(工作可能会导致您的应用程序变得无响应或janky)设置此参数时要谨慎。在哪里可以让浏览器决定何时调用回调。

使用 requestIdleCallback 发送分析数据

让我们来看看 requestIdleCallback 发送分析数据。在这种情况下,我们可能会想跟踪一个事件,比如说 - 点击导航菜单。不过,由于通常在萤幕上设定动画,我们希望避免立即将此事件发送到 Google Analytics(分析)。我们将创建一系列事件来发送和请求在将来的某个时间发送它们:

var eventsToSend = [];

function onNavOpenClick () {

  // Animate the menu.
  menu.classList.add('open');

  // Store the event for later.
  eventsToSend.push(
    {
      category: 'button',
      action: 'click',
      label: 'nav',
      value: 'open'
    });

  schedulePendingEvents();
}

现在我们需要使用 requestIdleCallback 来处理任何待处理的事件:

function schedulePendingEvents() {

  // Only schedule the rIC if one has not already been set.
  if (isRequestIdleCallbackScheduled)
    return;

  isRequestIdleCallbackScheduled = true;

  if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
  } else {
    processPendingAnalyticsEvents();
  }
}

在这里,您可以看到我设置了2秒的超时时间,但这个值取决于你的应用程序。对于分析数据,有意义的是,将使用超时时间来确保数据在合理的时间内报告,而不仅仅是在将来的某个时间点。

最后我们需要编写 requestIdleCallback 将执行的函数。

function processPendingAnalyticsEvents (deadline) {

  // Reset the boolean so future rICs can be set.
  isRequestIdleCallbackScheduled = false;

  // If there is no deadline, just run as long as necessary.
  // This will be the case if requestIdleCallback doesn’t exist.
  if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

  // Go for as long as there is time remaining and work to do.
  while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
  }

  // Check if there are more events still to send.
  if (eventsToSend.length > 0)
    schedulePendingEvents();
}

对于这个例子,我假设如果 requestIdleCallback 不存在分析数据应该立即发送。然而,在生产应用程序中,延迟发送超时可能会更好,以确保它不会与任何交互冲突,并导致 jank。

使用 requestIdleCallback 进行 DOM 更改

另一种 requestIdleCallback 真正有助于表现的情况是当您进行非必要的 DOM 更改时,例如将项目添加到不断增长的惰性列表的末尾。我们来看一下如何将 requestIdleCallback 实际应用在一个典型的框架。

https://developers.google.com/web/updates/images/2015-08-27-using-requestidlecallback/frame.jpg

浏览器可能太忙,无法在给定的框架中运行任何回调,所以您不应该期望在框架结尾处有任何空闲时间来执行任何更多的工作。这使得它不同于 setImmediate 这类每帧都运行的东西。

如果回调在框架结束时被触发,它将被调度到当前帧已经提交之后,这意味着将应用样式更改,并且重要的是布局计算。如果我们在空闲回调内部进行 DOM 更改,这些布局计算将无效。如果有任何读取下一帧布局的事件,例如 getBoundingClientRectclientWidth等等,浏览器将不得不进行强制同步布局,这是一个潜在的性能瓶颈。

不要在空闲回调中 DOM 更改的另一个原因是更改 DOM 的时间影响是不可预测的,因此我们可以很容易地超过浏览器提供的期限。

最好的做法是只能在 requestAnimationFrame 回调内部进行 DOM 更改,因为它是由浏览器安排的那种类型的工作。这意味着我们的代码将需要使用一个文档片段,然后可以将其附加在下一个 requestAnimationFrame 回调中。如果您正在使用 VDOM 库,则可以使用 requestIdleCallback 进行更改,但是您可以在下一个回调中应用 DOM 修补程序 requestAnimationFrame,而不是空闲回调。

所以考虑到这一点,让我们来看看代码:

function processPendingElements (deadline) {

  // If there is no deadline, just run as long as necessary.
  if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

  if (!documentFragment)
    documentFragment = document.createDocumentFragment();

  // Go for as long as there is time remaining and work to do.
  while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
  }

  // Check if there are more events still to send.
  if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

在这里我创建元素并使用该 textContent 属性来填充它,但是您的元素创建代码可能会更多的操作!在创建元素后 scheduleVisualUpdateIfNeeded,调用该元素将会建立一个单独的 requestAnimationFrame 回调,然后依次将文档片段附加到正文中:

function scheduleVisualUpdateIfNeeded() {

  if (isVisualUpdateScheduled)
    return;

  isVisualUpdateScheduled = true;

  requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
  // Append the fragment and reset.
  document.body.appendChild(documentFragment);
  documentFragment = null;
}

一切顺利,我们现在将把项目附加到 DOM 时看起来更少 jank。优秀!

常问问题

  • 是否有 polyfill? 可悲的是,没有。如果你想要透明的重定向到 setTimeout,那就有一个 shim 。该API存在的原因是因为它在Web平台中插入了非常真实的差距。但是没有JavaScript API来确定框架结束时的空闲时间,所以最多只能做出猜测。像 setTimeoutsetInterval 或者 setImmediate 这样的 API 可以用于调度工作,但是并不是按照 requestIdleCallback 这样的方式避开用户交互。
  • 如果我设置 timeout,会发生什么? 如果 timeRemaining() 返回零,但是您选择运行更长时间,您可以这样做,而不用担心浏览器停止工作。但是,浏览器为您提供了最终期限,以确保您的用户顺利体验,因此除非有非常好的理由,否则您应始终遵守最终期限。
  • timeRemaining() 会返回有最大值吗? 是的,现在是 50ms 。在尝试维护响应式应用程序时,所有对用户交互的响应应保持在 100ms 以下。如果用户在 50ms 窗口中进行交互,在大多数情况下应允许空闲回调完成,并且浏览器可以响应用户的交互。您可能会得到多个空闲的回调(如果浏览器确定有足够的时间来运行它们),那么它们将被一个个安排。
  • 有没有什么工作我不应该在 requestIdleCallback 里执行? 理想情况下,您所做的工作应该是具有相对可预测特征的小块(微任务)。例如,特别是更改 DOM 将具有不可预测的执行时间,因为它将触发样式计算,布局,绘画和合成。因此,您应该仅在上述 requestAnimationFrame 回调中进行 DOM 更改。要注意的另一件事是解决(或拒绝)Promises,因为在空闲回调完成后回调将立即执行,即使没有更多的时间剩余。
  • 我会总是在一帧结束时被执行 requestIdleCallback ?不,不总是。浏览器将在帧结束时的空闲时间或用户处于非活动状态的时段内调度回调。您不应该期望每帧调用回调,并且如果要求它在给定的时间范围内运行,则应该使用超时。
  • 我可以有多个 requestIdleCallback 回调吗? 是的,你可以拥有非常多个 requestAnimationFrame 回调。值得记住的是,如果你的第一个回调使用了它在回调期间的剩余时间,那么任何其他回调都不会有更多的时间。然后,其他回调将不得不等待浏览器下一个空闲,才能运行。根据您要完成的工作,可能会有一个空闲的回调,并将工作分配到那里。或者,您可以使用超时时间来确保回调都有机会执行。
  • 如果我在另一个内部设置一个新的空闲回调,会发生什么? 新的空闲回调将被安排尽快运行,从下一个帧开始(而不是当前的)。

利用空闲

requestIdleCallback 是一个非常棒的方法,以确保您可以运行您的代码,但没有阻碍用户的交互。使用起来很简单,非常灵活。但仍然属于早期,而且规范还没有完全解决,所以你所提供的任何反馈都是值得欢迎的。

【翻译原文】:useing requestIdleCallback

0%