本文同步公布在我的 Github博客
前言
最近在钻研 React Fiber 相干的常识,上一篇文章 浅谈对 React Fiber 的了解 简略提到了 requestIdleCallback, React 源码中 polyfill 了这个办法,理解它对 Fiber 也能有进一步了解。本篇会深刻介绍下这个办法。
requestIdleCallback 是什么
requestIdleCallback 是 window 属性上的办法,它的作用是在浏览器一帧的残余闲暇工夫内执行优先度绝对较低的工作。
为什么须要 requestIdleCallback
在网页运行中,有很多耗时但又不是那么重要的工作。这些工作和重要的工作如对用户的输出作出及时响应的之类的工作,它们共享事件队列。如果两者发生冲突,用户体验会很蹩脚。
requestIdleCallback 就解决了这个痛点,requestIdleCallback 会在每一帧完结时并且有闲暇工夫执行回调。
假如须要大量波及到 DOM 的操作的计算,在运算时,浏览器可能就会呈现显著的卡顿行为,甚至不能进行任何操作,因为是 JS 单线程,就算用在输出解决,给定帧渲染和合成之后,用户的主线程就会变得闲暇,直到下一帧的开始。
而这些闲暇工夫能够拿来解决低优先级的工作,React16 的调度策略异步可中断,其中要害就靠的这个(polyfill)办法性能;React 把工作细分(工夫切片),在浏览器闲暇的工夫去执行,从而尽可能地进步渲染性能。
工夫切片的实质是模仿实现 requestIdleCallback
讲到这里,从 React15 到 React16 Fiber,对整体性能来说是大优化了;但要晓得的是,React16 绝对 15 做出的优化,并不是大大减少了任务量,你写的代码的工作总量并没有变动,只是把闲暇工夫利用起来了,不停的干活,就能更快的把活干完;这只是其中一个角度,React 还做了辨别优先级执行等等。
屏幕刷新率和 FPS 的关系
以后大多数的屏幕刷新率都是 60HZ,一秒 60 帧(FPS 为 60),也就是每秒屏幕刷新 60 次,此时一帧的工夫为 16.7ms(1000ms/60)低于 60HZ 人眼就会感知卡顿掉帧等状况。
浏览器的一帧说的就是一次残缺的重绘。
前端浏览器所说的渲染频率 FPS(Frames Per Second)是每秒传输帧数,即是每秒刷新的次数,实践上 FPS 越高人眼感觉界面越晦涩。
window.requestIdleCallback() 用法
window.requestIdleCallback()办法将在浏览器的闲暇时段内调用的函数排队。这使开发者可能在主事件循环上执行后盾和低优先级工作,而不会影响提早要害事件,如动画和输出响应。函数个别会按先进先调用的程序执行,然而,如果回调函数指定了执行超时工夫 timeout,则有可能为了在超时前执行函数而打乱执行程序。
你能够在闲暇回调函数中调用 requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
var handle = window.requestIdleCallback(callback[, options])
返回一个 ID 标识符。
callback:一个在事件循环闲暇时行将被调用的函数的援用。函数会接管到一个名为 deadline 的参数,这个参数能够获取以后闲暇工夫以及回调是否在超时工夫前曾经执行的状态;该对象上有两个属性: - timeRemaining:timeRemaining属性是一个函数,函数的返回值示意返回以后闲暇工夫的剩余时间 - didTimeout:didTimeout属性是一个布尔值,如果didTimeout是true,那么示意本次callback的执行是因为超时的起因options 可选包含可选的配置参数。具备如下属性:timeout:如果指定了timeout并具备一个正值,并且尚未通过超时毫秒数调用回调,那么回调会在下一次闲暇期间被强制执行,只管这样很可能会对性能造成负面影响。
function myNonEssentialWork(deadline) { while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { doWorkIfNeeded() } if (tasks.length > 0) { requestIdleCallback(myNonEssentialWork) }}requestIdleCallback(myNonEssentialWork, 5000)
来看一个理论的例子:
requestIdleCallback(myWork)// 一个工作队列let tasks = [ function t1() { console.log('执行工作1') }, function t2() { console.log('执行工作2') }, function t3() { console.log('执行工作3') },]// deadline是requestIdleCallback返回的一个对象function myWork(deadline) { console.log(`以后帧剩余时间: ${deadline.timeRemaining()}`) // 查看以后帧的剩余时间是否大于0 && 是否还有残余工作 if (deadline.timeRemaining() > 0 && tasks.length) { // 在这里做一些事件 const task = tasks.shift() task() } // 如果还有工作没有被执行,那就放到下一帧调度中去继续执行,相似递归 if (tasks.length) { requestIdleCallback(myWork) }}
我的运行后果如下(每次运行,不同机器运行都不一样):
以后帧剩余时间: 15.120000000000001执行工作1以后帧剩余时间: 15.445000000000002执行工作2以后帧剩余时间: 15.21执行工作3
如果是因为 timeout 回调才得以执行的话,其实用户就有可能会感觉到卡顿了,因为一帧的执行工夫必然曾经超过 16ms 了
requestIdleCallback 办法的缺点
这个办法实践上可行,但为什么 React 团队又 polyfill 这个办法呢?
- 浏览器兼容不好的问题
- requestIdleCallback 的 FPS 只有 20,也就是 50ms 刷新一次,远远低于页面晦涩度的要求,所以 React 团队须要本人实现。
留神:timeRemaining 最大为 50ms,是有依据钻研得出的,即是说人对用户输出的 100 毫秒以内的响应通常被认为是刹时的,不会被人察觉到。将闲暇工夫限度在 50ms 内意味着即便在闲置工作开始后立刻产生用户操作,用户代理依然有残余的 50ms 能够在其中响应用户输出而不会产生用户可察觉的滞后。
requestIdleCallback 和 requestAnimationFrame 区别
- requestAnimationFrame 的回调会在每一帧确定执行,属于高优先级工作
- requestIdleCallback 的回调则不肯定,有闲暇工夫才执行,属于低优先级工作。
window.requestAnimationFrame() 通知浏览器——你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该办法须要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
requestAnimationFrame 比起 setTimeout、setInterval 的劣势次要有两点:
- requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就实现,并且重绘或回流的工夫距离紧紧追随浏览器的刷新频率,一般来说,这个频率为每秒 60 帧。
- 在暗藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的的 cpu,gpu 和内存使用量。
模仿实现 requestIdleCallback
setTimeout 模仿
当然个别不会用 setTimeout 的,因为误差很大
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)}
React 团队则是用 requestAnimationFrame 和 postMessage 模仿实现的,本篇就不讲了,有趣味的能够去理解看看。
参考
- 相熟 requestidlecallback 到理解 react ric polyfill 实现
- react 中 requestIdleCallback 的实现原理
我近期会保护的开源我的项目:
- 基于 React + TypeScript + Dumi + Jest + Enzyme 开发 UI 组件库
- Next.js 企业级我的项目脚手架模板
- 集体技术博文 Github 仓库
感觉不错的话欢送 star,给我一点激励持续写作吧~