【译】使用requestIdleCallback

英文原文:https://developers.google.com/web/updates/2015/08/27/using-requestidlecallback(备好梯子)

如有不当之处,还请指正。

概览:

requestIdleCallback是一个当浏览器处于闲置状态时,调度工作的新的性能相关的API

正文:

如今,大多数的站点和app都需要执行很多的js脚本。你的js通常需要尽可能快地执行,而且,你又不希望通过获取用户行为的方式来达成目的。如果当用户滚动页面的时候,你的JS开始上报数据,或者当用户点击按钮的时候,你往DOM中添加元素,你的web应用其实就已经变得迟钝,导致很差的用户体验。

现在有个好消息,一个新的API能够帮助你: requestIdleCallback 。跟requestAnimationFrame一样,requestAnimationFrame允许我们正确地安排动画,同时最大限度地去提升到60fps。而requestIdleCallback则会在某一帧结束后的空闲时间或者用户处于不活跃状态时,处理我们的工作。这表明在不获取用户行为条件下,你能执行相关的工作。目前这个新的API在Chrome Canary(M46+)下可用(需要打开chrome://flags/#enable-experimental-web-platform-features 去开启该功能),这样你从今天开始先尝试玩玩。但要记着,这个API是一个实验性的功能,该规范仍在不断变化,所以任何东西都可能随时改变。

为什么我要使用requestIdleCallback?

靠自己人工的安排不必要的工作是很困难的。比如,要弄清楚一帧剩余的时间,这显然是不可能的,因为当requestAnimationFrame的回调完成后,还要进行样式的计算,布局,渲染以及浏览器内部的工作等等。上面的话貌似还不能说明什么。为了确保用户不以某种方式进行交互,你需要为各种交互行为添加监听事件(scroll、touch、click),即使你并不需要这些功能,只有这样才能绝对确保用户没有进行交互。另一方面,浏览器能够确切地知道在一帧的结束时有多少的可用时间,如果用户正在交互,通过使用requestIdleCallback这个API,允许我们尽可能高效地利用任何的空闲时间。

接下来让我们看看它的更多细节,且让我们知道如果使用它。

检查requestIdleCallback

目前requestIdleCallback这个API仍处于初期,所以在使用它之前,你应该检查它是否可用。

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

现在,我们假设已经支持该API

使用requestIdleCallback

调用requestIdleCallback跟调用requestAnimationFrame十分相似,它需要把回调函数作为第一个参数:

requestIdleCallback(myNonEssentialWork);

当 myNonEssentialWork 被调用,会返回一个 deadline 对象,这个对象包含一个方法,该方法会返回一个数字表示你的工作还能执行多长时间:

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

调用 timeRemaining 这个方法能获得最后的剩余时间,当 timeRemaining() 返回0,如果你仍有其他任务需要执行,你便可以执行另外的requestIdleCallback:

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

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

确保你的方法已被调用

当事件很多的时候,你会怎么做?你可能会担心你的回调函数永远不被执行。很好,尽管requestIdleCallback跟requestAnimationFrame很像,但它们也有不同,在于requestIdleCallback有一个可选的第二个参数:含有timeout属性的对象。如果设置了timeout这个值,回调函数还没被调用的话,则浏览器必须在设置的这个毫秒数时,去强制调用对应的回调函数。

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

如果你的回调函数是因为设置的这个timeout而触发的,你会注意到:

  • timeRemaining()会返回0
  • deadline对象的didTimeout属性值是true

如果你发现didTimeout是true,你的代码可能会是这样子的:

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

因为设置timeout对你用户导致的潜在破坏(这个操作会使你的app变得迟钝且低质量),请小心地设置这个参数。所以,在这,就让浏览器自己去决定什么时候触发回调吧。

使用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秒的超时,但取决于你的应用。因为对于上报的这些分析数据,设置一个timeout来确保数据在一个合理的时间范围内被上报,而不是延迟到某个未知的时间点。这样做才是合理且有意义的。

最后我们来写下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,则立即上报数据。然而,对于一个在生产环境的应用,最好是用timeout延迟上报来确保不跟任何相互冲突。

使用requestIdleCallback改变dom

requestIdleCallback可以帮助提高性能的另一个场景是,当你需要做一些非必要的dom改动,比如懒加载,滚动页面时候不断在尾部添加元素。让我们看看requestIdleCallback事实上是如何插入一帧里的。

对于浏览器,在给定的一帧内因为太忙而没有去执行任何回调这是有可能的,所以你不应该期望在一帧的末尾有空闲的时间去做任何事。这一点就使得requestIdleCallback跟setImmediate不太像,setImmediate是在每一帧里都会执行。

如果在某一帧的末尾,回调函数被触发,它将被安排在当前帧被commit之后,这表示相应的样式已经改动,同时更最重要的,布局已经重新计算。如果我们在这个回调中进行样式的改动,涉及到的布局计算则会被判无效。如果在下一帧中有任何的读取布局相关的操作,例如getBoundingClientRect,clientWidth等等,浏览器会不得不执行一次强制同步布局(Forced Synchronous Layout),这将是一个潜在的性能瓶颈。

另一个不要在回调中触发Dom改动的原因是,Dom改动是不可预期的,正因为如此,我们可以很容易地超过浏览器给出的时间限期。

最佳的实践就是只在requestAnimationFrame的回调中去进行dom的改动,因为浏览器会优化同类型的改动。这表明我们的代码要在requestIdleCallback时使用文档片段,这样就能在下一个requestAnimationFrame回调中把所有改动的dom追加上去。如果你正在使用Virtual DOM这个库,你可以使用requestIdleCallback进行Dom变动,但真正的Dom改动还是在下一个requestAnimationFrame的回调中,而不是requestIdleCallback的回调中。

所以谨记上面说的,下面来看下代码吧:

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的回调,这时候,我们就应该把文档片段追加到body中去了:

function scheduleVisualUpdateIfNeeded() {

  if (isVisualUpdateScheduled)
    return;

  isVisualUpdateScheduled = true;

  requestAnimationFrame(appendDocumentFragment);
}

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

一切顺利的话,我们则会看到追加dom到文档中时,并没有什么性能的损耗。真tm的棒!

时间: 2024-10-25 04:38:55

【译】使用requestIdleCallback的相关文章

译:SOS_SCHEDULER_YIELD类型等待在虚拟机环境中的增多

原文出处:Increased SOS_SCHEDULER_YIELD waits on virtual machines 注: 原文的用词是Increased,想译作增强(增长),或者加强,这么译起来是褒义词,而原文要表达的Increased并没有褒义的含义,最起码是一个中性的含义,想来想起用一个“滋长”偏编译的含义还是比较合适的,感觉还是有点过于贬义了,还是用最通俗的增多吧.个人英语水平有限,另外就是对于文中提到的“rdtsc周期”也不是非常清楚,翻译的也不是很清楚,权当是自娱自乐.总是原文的

《100种过度医疗大公开》:转译自日文版,日文版依据的是美国的“Choosing Wisely”项目。三星推荐

本书转译自日文,日文版则是在美国的“Choosing Wisely”项目中选择了100个相对常见的过度医疗项目做解说.Choosing Wisely项目,是由美国多个专业医学组织发起的列出过度医疗项目的活动. 日文作者是兽医专业的新闻记者,中文译者不是医学专业人员,个别专业词语翻译有误,“随机对照试验”翻译成“随机比较试验”了,有些句子翻译的也比较别扭.基于以上两点,这本书的权威性可信度我认为都不算太高,只能给三星.感兴趣的话还是应该直接上网看英文原版. 以下是书中部分信息的摘抄: 1:以美国内

博译有道——关于外文书籍的翻译

有人说,中国古代曾经被人津津乐道的诸如木流牛马之类的工匠发明和创造之所以没有被后人所重现,是因为创作者只知其术,不懂传道.在门户之见成风,传道授艺大多通过手把手教.口口相传的古代,可想而知像<本草纲目>这样的著作的重要性.同样在今天,无论是原创,还是转载,通过互联网广泛传播碎片化知识的同时,作为系统性的.汇总性的书籍显得颇为难能可贵,即便是将一本国外早已出版的书籍引入到国内出版. 经过长达六个月的付出和守候,由IDF实验室成员翻译.机械工业出版社出版的<Hacking with Kali

[email&#160;protected]一个高效的配置管理工具--Ansible configure management--翻译(六)

无书面许可请勿转载 高级playbook Finding files with variables All modules can take variables as part of their arguments by dereferencing them with {{ and }} . You can use this to load a particular file based on a variable. For example, you might want to select a

[译]JavaScript中,{}+{}等于多少?

[译]JavaScript中,{}+{}等于多少? 原文:http://www.2ality.com/2012/01/object-plus-object.html 最近,Gary Bernhardt在一个简短的演讲视频“Wat”中指出了一个有趣的JavaScript怪癖:在把对象和数组混合相加时,会得到一些你意想不到的结果.本篇文章会依次讲解这些计算结果是如何得出的. 在JavaScript中,加法的规则其实很简单,只有两种情况:你只能把数字和数字相加,或者字符串和字符串相加,所有其他类型的值

[译]WordPress 4.3 将会用node.js重写

一个核心的wordpress开发者Ryan Boren表示,wordpress4.3 cms系统将会有一个重大的变革,那就是用node.js重写,同时完全兼容之前的版本. WordPress 和 Node.js 开发者以及项目本身已经关注node.js很久了,当然,这早就不是什么秘密了,node.js允许你用javascript写服务端代码. 我们首次使用node.js是在3.7版本中,同时也把Grunt工具加入了我们的开发周期中.自那以后,越来越多的核心版本开始使用javascript,NB的

storm源码之storm代码结构【译】【转】

[原]storm源码之storm代码结构[译] 说明:本文翻译自Storm在GitHub上的官方Wiki中提供的Storm代码结构描述一节Structure of the codebase,希望对正在基于Storm进行源码级学习和研究的朋友有所帮助. Storm的源码共分为三个不同的层次. 首先,Storm在设计之初就考虑到了兼容多语言开发.Nimbus是一个thrift服务,topologies被定义为Thrift结构体.Thrift的运用使得Storm可以被任意开发语言使用. 其次,Stor

AFNnetworking快速教程,官方入门教程译

AFNnetworking快速教程,官方入门教程译 分类: IOS2013-12-15 20:29 12489人阅读 评论(5) 收藏 举报 afnetworkingjsonios入门教程快速教程 AFNetworking官网入门教程简单翻译,学习 AFNetworking 是一个能够快速使用的ios和mac os x下的网络框架,它是构建在Foundation URL Loading System之上的,封装了网络的抽象层,可以方便的使用,AFNetworking是一个模块化架构,拥有丰富ap

[译]最长回文子串(Longest Palindromic Substring) Part II

[译+改]最长回文子串(Longest Palindromic Substring) Part II 问题:给定字符串S,求S中的最长回文子串. 在上一篇,我们给出了4种算法,其中包括一个O(N2)时间O(1)空间的算法(中心检测法),已经很不错了.本篇将讨论一个O(N)时间O(N)空间的算法,即著名的Manacher算法,并详细说明其时间复杂度为何是O(N). 提示 +BIT祝威+悄悄在此留下版了个权的信息说: 先想想有什么办法能改进中心检测法. 考虑一下最坏的情况.★ 最坏的情况就是各个回文