大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

许多网站和应用程序都有大量脚本要执行。 JavaScript 通常需要尽快运行,但同时又不希望妨碍用户。
如果在用户滚动页面时发送分析数据,或者在用户点击按钮时将元素附加到 DOM,则 Web 应用程序可能会变得无响应,从而导致用户体验不佳。
现在浏览器有一个 API 可以解决这类问题,即 requestIdleCallback。 与 requestAnimationFrame 类似(正确处理动画并最大限度保证 60fps 流畅度),requestIdleCallback 将在当前帧结束时有空闲时间或用户不活动时自动处理工作。
这意味着开发者有机会在不妨碍用户的情况下完成工作。 该方法从 Chrome 47 开始可用。但值得注意的是,requestIdleCallback 目前是一项实验性功能,并且规范仍在不断变化。
什么是 requestAnimationFrame(rAF)requestAnimationFrame() 告诉浏览器希望执行动画,并请求浏览器在下次重绘(repaint,和 reflow 有区别)之前调用指定的函数来更新动画。
值得注意的是,如果想在下一次重绘时为另一个帧设置动画,回调本身必须调用 requestAnimationFrame()。每当准备好更新屏幕上的动画时,都应该调用此方法从而实现以下目的:
浏览器可以优化,动画会更流畅非活动选项卡中的动画将停止,让出 CPU对电池更友好1.什么需要 requestIdleCallback开发者自己安排非必要的工作是非常困难的,也不可能准确计算出还剩多少运行时间,因为在执行 requestAnimationFrame 回调后,还需要运行样式计算、布局、绘制和其他浏览器内部功能。
requestIdleCallback() 方法对要在浏览器空闲期间调用的函数进行排队。 这使开发人员能够在主事件循环上执行后台和低优先级工作,而不会影响动画和输入响应等延迟关键事件。
函数一般按照先进先出的顺序调用。 但如果指定了 timeout,回调可能会被乱序调用,以便在超时结束之前运行。开发者甚至可以在空闲回调函数中调用 requestIdleCallback() 来安排另一个回调在下一次循环之前发生。
总之,通过 requestIdleCallback 开发者能确切知道帧结束还有多少可用时间,以及用户是否正在交互(否则需要 attach 众多无意义的事件),可以最大限度利用任何空闲时间。
2.检查 requestIdleCallback 支持情况根据 caniuse 的数据,requestIdleCallback 在 Chrome>=47、Edege>79、FireFox>=55、Opera>34 版本上都已经可用,而 Safari 在 13.1 以上版本添加了实验性支持,总体支持力度 76.58%。

为了保证最大的浏览器兼容性,在使用之前应该检查是否可用:
if ('requestIdleCallback' in window) { // 可用requestIdleCallback} else { // 常规处理}对于不支持的浏览器,还可以调整其行为,比如:回退到 setTimeout:
// https://gist.github.com/paullewis/55efe5d6f05434a96c36window.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 || // 浏览器默认支持cancelIdleCallback function (id) { clearTimeout(id); };使用 setTimeout 的 shim 方式可能并不理想,因为不如 requestIdleCallback 了解浏览器空闲时间状态,但是能在一定程度上守住底线。
3.如何使用 requestIdleCallback调用 requestIdleCallback 与 requestAnimationFrame 方法非常相似,采用 callback 作为第一个参数。
requestIdleCallback(myNonEssentialWork);myNonEssentialWork 是对应该在不久的将来事件循环空闲时调用的函数的引用。 回调函数接收一个 IdleDeadline 对象,该对象描述可用时间量以及回调是否因超时期限到期而已经运行。
function myNonEssentialWork(deadline) { // deadline为0依然可以运行JS,但是很可能打扰用户,不太友好 while (deadline.timeRemaining() > 0) doWorkIfNeeded();}开发者也可以调用 timeRemaining 函数来获取最新的值。当 timeRemaining() 返回零时,如果还有更多工作要做,可以安排另一个 requestIdleCallback:
// myNonEssentialWork表示所有优先级比较低的任务function myNonEssentialWork(deadline) { while (deadline.timeRemaining() > 0 && tasks.length > 0) doWorkIfNeeded(); if (tasks.length > 0) requestIdleCallback(myNonEssentialWork); //放在下一次循环}4.保证 requestIdleCallback 回调函数被调用开发者可能会担心回调永远不会被调用。
虽然 requestIdleCallback 类似于 requestAnimationFrame,但不同之处在于它接收一个可选的第二个参数:具有 timeout 属性的选项对象。 如果设置了此 timeout,则会为浏览器提供一个必须执行回调的时间(以毫秒为单位):
// 处理事件之前最多等待2srequestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });如果回调由于超时触发而被执行,此时回调参数调用结果会是:
timeRemaining() 将返回零deadline 对象的 didTimeout 属性将为 true因此,如果看到 didTimeout 为 true,可以按照下面的方法处理:
function myNonEssentialWork(deadline) { // 最大限度榨干任何剩余时间,或者超时仅运行完成任务 while ( (deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0 ) doWorkIfNeeded(); if (tasks.length > 0) requestIdleCallback(myNonEssentialWork);}由于此超时可能会对用户造成潜在的干扰,因此开发者应该尽可能让浏览器决定何时调用回调。
5.使用 requestIdleCallback 发送 Google Analytics 分析数据接下来使用 requestIdleCallback 发送网站分析数据。
在日常开发中可能想要跟踪一个事件,比如:点击导航菜单。 但是,由于站点通常会有动画,因此通常希望尽量晚发送此类事件,这就是 requestIdleCallback 的另一个用武之地。
var eventsToSend = [];function onNavOpenClick() { // 动画事件 menu.classList.add('open'); // 存储事件系列 eventsToSend.push({ category: 'button', action: 'click', label: 'nav', value: 'open', }); schedulePendingEvents();}接下来可以使用 requestIdleCallback 来处理任何待处理的事件:
function schedulePendingEvents() { if (isRequestIdleCallbackScheduled) return; isRequestIdleCallbackScheduled = true; if ('requestIdleCallback' in window) { // 超时2s requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 }); } else { processPendingAnalyticsEvents(); }}上面代码设置了 2 秒的超时,但该值取决于应用程序本身。对于追踪数据来说,超时设置是有意义的。最后编写 requestIdleCallback 将执行的函数。
function processPendingAnalyticsEvents(deadline) { isRequestIdleCallbackScheduled = false; // 如果没有deadline,则只要需要就运行。 // 如果 requestIdleCallback 不存在,就会出现这种情况 if (typeof deadline === 'undefined') deadline = { timeRemaining: function () { return Number.MAX_VALUE; }, }; // 榨干剩余时间 while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) { var evt = eventsToSend.pop(); ga('send', 'event', evt.category, evt.action, evt.label, evt.value); } // 如果没执行完放在下一帧 if (eventsToSend.length > 0) schedulePendingEvents();}上面示例假设 requestIdleCallback 不存在,则应立即发送分析数据。 然而,在生产环境中,最好通过 timeout 来延迟发送,以确保不会与任何交互发生冲突并导致卡顿。
6.DOM 更改优先 requestAnimationFrame 而非 requestIdleCallbackrequestIdleCallback 真正可以提高性能的另一种情况是当需要进行不必要的 DOM 更改时,例如:动态懒加载新增元素到页面末尾。
接下来一起看看 requestIdleCallback 是如何融入框架的。

浏览器可能太忙而无法在给定帧中运行任何 callback,因此开发者不应期望在帧结束时会有任何过多的空闲时间来执行更多工作。 这使得它与像 setImmediate 这样的东西不同,后者确实每帧运行。
对于 requestIdleCallback 回调中的 DM 操作来说,有以下潜在问题:
如果 callback 在帧末尾触发,其将被安排在当前帧提交之后,此时浏览器需要应用样式更改,并且重新计算布局。 如果在空闲回调中进行 DOM 更改,这些布局计算将失效。 如果下一帧中有任何类型的布局读取,例如: getBoundingClientRect、clientWidth 等,浏览器将不得不执行强制同步布局,这是潜在的性能瓶颈。更改 DOM 的时间影响不可预测(牵涉:样式计算、布局、绘制和合成),因此很容易超过浏览器提供的 deadline。因此,最佳实践是在 requestAnimationFrame 回调中进行 DOM 更改,因为可以由浏览器本身调度。 此时,需要使用文档片段(document fragment),然后可以将其附加到下一个 requestAnimationFrame 回调中。
如果使用的是 VDOM 库,可以使用 requestIdleCallback ,但其将在下一个 requestAnimationFrame 回调而非空闲回调(idle callback)中应用 DOM 补丁(DOM patches)。
考虑到这一点,看一下如下代码:
function processPendingElements(deadline) { // 如果没有deadline正常执行 if (typeof deadline === 'undefined') deadline = { timeRemaining: function () { return Number.MAX_VALUE; }, }; // 使用文档片段 if (!documentFragment) documentFragment = document.createDocumentFragment(); // 榨干剩余时间 while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) { // 创建元素 var elToAdd = elementsToAdd.pop(); var el = document.createElement(elToAdd.tag); el.textContent = elToAdd.content; // 添加到fragment documentFragment.appendChild(el); // 不要立即追加到文档,等待下一个requestAnimationFrame回调 scheduleVisualUpdateIfNeeded(); } // 其余的放在下一帧 if (elementsToAdd.length > 0) scheduleElementCreation();}上面示例创建元素并填充了 textContent 属性,但框架本身的元素创建逻辑可能会更加复杂。 创建元素之后,scheduleVisualUpdateIfNeeded 被调用,其将设置一个 requestAnimationFrame 回调,该回调将依次将文档片段附加到 body:
// 将document fragment附加在bodyfunction scheduleVisualUpdateIfNeeded() { if (isVisualUpdateScheduled) return; isVisualUpdateScheduled = true; requestAnimationFrame(appendDocumentFragment);}function appendDocumentFragment() { // Append the fragment and reset. document.body.appendChild(documentFragment); documentFragment = null;}7.requestIdleCallback 常见疑问polyfill目前没有 polyfill,但如果想透明地重定向到 setTimeout,可以使用下面的 shim。
1.shim 是能用的补丁
2.sham 顾名思义,是假的意思,所以 sham 是一些假的方法,只能使用保证不出错,但不能用。至于为啥会有 sham,因为有些方法的低端浏览器里根本实现不了
window.requestIdleCallback = window.requestIdleCallback || function (cb) { return setTimeout(function () { var start = Date.now(); cb({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50 - (Date.now() - start)); }, }); }, 1); };window.cancelIdleCallback = window.cancelIdleCallback || function (id) { clearTimeout(id); };这个 API 存在的原因是它填补了 Web 平台中的一个非常现实的空白。 推断当前浏览器是否在活动状态(Lack of Activity)很困难,而且根本不存在 JavaScript API 来确定帧结束时的空闲时间量,因此最多只能进行猜测。 像 setTimeout、setInterval 或 setImmediate 这样的 API 可用于安排工作,但它们不会像 requestIdleCallback 那样进行定时以避免用户交互。
timeRemaining() 是否有返回最大值目前是 50 毫秒。
当尝试维护响应式应用程序时,对用户交互的所有响应都应保持在 100 毫秒以下。 在大多数情况下,如果用户进行交互,50 毫秒窗口应该允许空闲回调完成,并允许浏览器响应用户的交互。
注意:可能会获得连续安排的多个空闲回调,前提是浏览器确定有足够的时间来运行。
其他注意事项Promise 的 resolve 或 reject 会在空闲回调完成后立即执行,即使没有更多的剩余时间如果第一个回调用完了回调期间的剩余时间,那么将没有更多的时间用于任何其他回调。 其他回调必须等到浏览器下次空闲时才能运行。参考资料https://developer.chrome.com/blog/using-requestidlecallback/
https://www.w3schools.com/jsref/met_win_requestanimationframe.asp
https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
https://www.w3.org/TR/requestidlecallback/
https://www.scaler.com/topics/react/react-fiber/