原文地址:http://galen-yip.com/2015/10/07/%E3%80%90%E8%AF%91%E3%80%91%E4%BD%BF%E7%94%A8requestIdleCallback

英文原文: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仍处于初期,所以在使用它之前,你应该检查它是否可用。

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

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

使用requestIdleCallback

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

  1. requestIdleCallback(myNonEssentialWork);

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

  1. function myNonEssentialWork (deadline) {
  2. while (deadline.timeRemaining() > 0)
  3. doWorkIfNeeded();
  4. }

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

  1. function myNonEssentialWork (deadline) {
  2. while (deadline.timeRemaining() > 0 && tasks.length > 0)
  3. doWorkIfNeeded();
  4.  
  5. if (tasks.length > 0)
  6. requestIdleCallback(myNonEssentialWork);
  7. }

确保你的方法已被调用

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

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

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

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

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

  1. function myNonEssentialWork (deadline) {
  2.  
  3. // Use any remaining time, or, if timed out, just run through the tasks.
  4. while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
  5. tasks.length > 0)
  6. doWorkIfNeeded();
  7.  
  8. if (tasks.length > 0)
  9. requestIdleCallback(myNonEssentialWork);
  10. }

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

使用requestIdleCallback去上报数据

让我们试试用requestIdleCallback去上报数据。在这种情况下,我们可能希望去跟踪一个事件,如“点击导航栏菜单”。然而,因为通常他们是通过动画展现在屏幕上的,我们希望避免立即发送事件到Google Analytics,因此我们将创建一个事件的数组来延迟上报,且在未来的某个时间点会发送出去。

  1. var eventsToSend = [];
  2.  
  3. function onNavOpenClick () {
  4.  
  5. // Animate the menu.
  6. menu.classList.add('open');
  7.  
  8. // Store the event for later.
  9. eventsToSend.push(
  10. {
  11. category: 'button',
  12. action: 'click',
  13. label: 'nav',
  14. value: 'open'
  15. });
  16.  
  17. schedulePendingEvents();
  18. }

现在我们使用requestIdleCallback来处理那些被挂起的事件。

  1. function schedulePendingEvents() {
  2.  
  3. // Only schedule the rIC if one has not already been set.
  4. if (isRequestIdleCallbackScheduled)
  5. return;
  6.  
  7. isRequestIdleCallbackScheduled = true;
  8.  
  9. if ('requestIdleCallback' in window) {
  10. // Wait at most two seconds before processing events.
  11. requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
  12. } else {
  13. processPendingAnalyticsEvents();
  14. }
  15. }

上面代码中,你可以看到我设置了2秒的超时,但取决于你的应用。因为对于上报的这些分析数据,设置一个timeout来确保数据在一个合理的时间范围内被上报,而不是延迟到某个未知的时间点。这样做才是合理且有意义的。

最后我们来写下requestIdleCallback执行的回调方法:

  1. function processPendingAnalyticsEvents (deadline) {
  2.  
  3. // Reset the boolean so future rICs can be set.
  4. isRequestIdleCallbackScheduled = false;
  5.  
  6. // If there is no deadline, just run as long as necessary.
  7. // This will be the case if requestIdleCallback doesn’t exist.
  8. if (typeof deadline === 'undefined')
  9. deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
  10.  
  11. // Go for as long as there is time remaining and work to do.
  12. while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
  13. var evt = eventsToSend.pop();
  14.  
  15. ga('send', 'event',
  16. evt.category,
  17. evt.action,
  18. evt.label,
  19. evt.value);
  20. }
  21.  
  22. // Check if there are more events still to send.
  23. if (eventsToSend.length > 0)
  24. schedulePendingEvents();
  25. }

这个例子中,我假设如果不支持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的回调中。

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

  1. function processPendingElements (deadline) {
  2.  
  3. // If there is no deadline, just run as long as necessary.
  4. if (typeof deadline === 'undefined')
  5. deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
  6.  
  7. if (!documentFragment)
  8. documentFragment = document.createDocumentFragment();
  9.  
  10. // Go for as long as there is time remaining and work to do.
  11. while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {
  12.  
  13. // Create the element.
  14. var elToAdd = elementsToAdd.pop();
  15. var el = document.createElement(elToAdd.tag);
  16. el.textContent = elToAdd.content;
  17.  
  18. // Add it to the fragment.
  19. documentFragment.appendChild(el);
  20.  
  21. // Don't append to the document immediately, wait for the next
  22. // requestAnimationFrame callback.
  23. scheduleVisualUpdateIfNeeded();
  24. }
  25.  
  26. // Check if there are more events still to send.
  27. if (elementsToAdd.length > 0)
  28. scheduleElementCreation();
  29. }

在上面,我创建了一个元素,而且使用添加上了textContent这个属性。但这时候还不应该把元素追加到文档流中去。创建完元素添加到文档片段后,scheduleVisualUpdateIfNeeded则被调用,它会创建一个requestAnimationFrame的回调,这时候,我们就应该把文档片段追加到body中去了:

  1. function scheduleVisualUpdateIfNeeded() {
  2.  
  3. if (isVisualUpdateScheduled)
  4. return;
  5.  
  6. isVisualUpdateScheduled = true;
  7.  
  8. requestAnimationFrame(appendDocumentFragment);
  9. }
  10.  
  11. function appendDocumentFragment() {
  12. // Append the fragment and reset.
  13. document.body.appendChild(documentFragment);
  14. documentFragment = null;
  15. }

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

【译】使用requestIdleCallback的更多相关文章

  1. [译] 尤雨溪:Vue 3.0 计划

    [译] 尤雨溪:Vue 3.0 计划 原文:Plans for the Next Iteration of Vue.js作者:Evan You 发表时间:Sep 30, 2018译者:西楼听雨 发表时 ...

  2. RxJS + Redux + React = Amazing!(译一)

    今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...

  3. Entity Framework 6 Recipes 2nd Edition 译 -> 目录 -持续更新

    因为看了<Entity Framework 6 Recipes 2nd Edition>这本书前面8章的翻译,感谢china_fucan. 从第九章开始,我是边看边译的,没有通读,加之英语 ...

  4. RxJS + Redux + React = Amazing!(译二)

    今天,我将Youtube上的<RxJS + Redux + React = Amazing!>的后半部分翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: ht ...

  5. 「译」JUnit 5 系列:条件测试

    原文地址:http://blog.codefx.org/libraries/junit-5-conditions/ 原文日期:08, May, 2016 译文首发:Linesh 的博客:「译」JUni ...

  6. CSharpGL(31)[译]OpenGL渲染管道那些事

    CSharpGL(31)[译]OpenGL渲染管道那些事 +BIT祝威+悄悄在此留下版了个权的信息说: 开始 自认为对OpenGL的掌握到了一个小瓶颈,现在回头细细地捋一遍OpenGL渲染管道应当是一 ...

  7. [译]基于GPU的体渲染高级技术之raycasting算法

    [译]基于GPU的体渲染高级技术之raycasting算法 PS:我决定翻译一下<Advanced Illumination Techniques for GPU-Based Volume Ra ...

  8. Entity Framework 6 Recipes 2nd Edition(9-4)译->Web API 的客户端实现修改跟踪

    9-4. Web API 的客户端实现修改跟踪 问题 我们想通过客户端更新实体类,调用基于REST的Web API 服务实现把一个对象图的插入.删除和修改等数据库操作.此外, 我们想通过EF6的Cod ...

  9. Entity Framework 6 Recipes 2nd Edition(10-1)译->非Code Frist方式返回一个实体集合

    存储过程 存储过程一直存在于任何一种关系型数据库中,如微软的SQL Server.存储过程是包含在数据库中的一些代码,通常为数据执行一些操作,它能为数据密集型计算提高性能,也能执行一些为业务逻辑. 当 ...

随机推荐

  1. poj 1962 Corporative Network

    主题链接:http://poj.org/problem?id=1962 思路:每一个集合中用根节点标记这个集合,每一个点到根节点的距离. code: <span style="font ...

  2. Unity判断网络连接类型

    使用NetworkReachability判断手机游戏当前的网络连接类型,是wifi还是234G using UnityEngine; using System.Collections; public ...

  3. android得知----overridePendingTransition

    1 Activity动画是指从一个切换activity跳到另一个activity随着电影. 它由两部分组成:第一部分是一个activity动画出口:中的第二个另一部分activity动画被访问: 于A ...

  4. 了解HTML5和“她”的 API (二)

    Communication(通信)     Cross Document Messaging (跨文档消息通信) postMessage API   origin security(源安全) chat ...

  5. 【Android进阶】ZXing android 错误(Could not find class 'com.google.zxing.ResultPoint)

    解决方法: 1.右键工程Build path, java build path,选择libraries 在右边的按钮中点击"Add Library" 选择"User li ...

  6. aauto攫http数据

    说明:灵巧使用上述数据的抓取网页简单的例子. 样例:想把某站点的数据抓回来.然后保存在数据库里边. 步骤:1.打开sql.new一个数据库Test.新建一个表test. 2.打开快手.准备抓数据,以这 ...

  7. Source insight 3572安装和版本号An invalid source insight serial number was detected解

    Source insight最新版本3572 下载链接:http://www.sourceinsight.com/down35.html,   http://www.sourceinsight.com ...

  8. java中的移位运算符:<<,>>,>>>总结(转)

    java中有三种移位运算符 <<      :     左移运算符,num << 1,相当于num乘以2 >>      :     右移运算符,num >& ...

  9. Chapter06-Phylogenetic Trees Inherited(POJ 2414)(减少国家DP)

    Phylogenetic Trees Inherited Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 480 Accepted ...

  10. Servlet实例解说

    打开昨天上午,负责人突然问我,client控制信息,如何让在后台?我想回答:假设总体提交form,在C#使用代码request获取表单的内容.假设局部提交,在用JS和Ajax交互,通过Ajax的ope ...