requestAnimationFrame 与 requestIdleCallback

作者: 贺鹏飞 分类: JavaScript,数据可视化 发布时间: 2021-08-20 16:49

前言

1.视觉暂留

眼睛的另一个重要特是视觉惰,即光象一旦在视网膜上形成,视觉将会对这个光象的感觉维持一个有限的时间,这种生理现象叫做视觉暂留。对于中等亮度的光刺激,视觉暂留时间约为50ms200ms。当我们看屏幕的时候,虽然你什么也没做,但是屏幕还是以特定的频率在不停刷新,只是这个刷新过程我们肉眼识别到他的细微变化,这就是我们接下来要说的 屏幕刷新频率

2.屏幕刷新频率

我们日常的显示器,一般频率在60Hz左右,意味着我们的屏幕每1秒需要刷新60次,也就是说每1000ms需要更新60次的屏幕图像,那么我们由此可以得出,屏幕图像更新一次所需要的时间间隔也就是16.7ms(1000/60≈16.7)
由于人的眼睛具有视觉暂留效应,且暂留时间为50ms200ms,也就是说人在看屏幕的时候,还没等到你的大脑印象消失,电脑屏幕就已经更新了,所以这个间隔让你感觉不到变化。
那么屏幕刷新频率是不是越大越好?我们可以大胆假设一下,假如我有三个显示器,刷新频率分别为1Hz60Hz200Hz、那么对应的更新周期时间分别为1000ms16.7ms5ms也就是频率越大,图像更新的间隔就越短,我们看到的画面就会越稳定,当达到一秒更新一次的时候,这个时候我们就能够感觉到明显的屏幕闪烁,带来视觉疲劳。

setTimeout相比,requestAnimationFrame最大的优势是由浏览器来决定回调函数的执行时机,即紧跟浏览器的刷新步调。

具体一点讲,如果屏幕刷新频率是60Hz,那么回调函数每16.7ms被执行一次,如果屏幕刷新频率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,自然不会导致动画的卡顿。

requestAnimationFrame

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。

设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。

语法:

window.requestAnimationFrame(callback);

参数

callback下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
            window.setTimeout(callback, 1000 / 60);
        };
})();

上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame

requestAnimationFrame 比起 setTimeout、setInterval 的优势有两点:

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就实现,并且重绘或回流的工夫距离紧紧追随浏览器的刷新频率,一般来说,这个频率为每秒 60 帧。
  • 在暗藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的的 cpu,gpu 和内存使用量。

requestIdleCallback

MDN上的解释:requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

语法

var handle = window.requestIdleCallback(callback[, options])

返回值

一个ID,可以把它传入 Window.cancelIdleCallback() 方法来结束回调。

参数

callback一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。options 可选包括可选的配置参数。具有如下属性:

  • timeout: 如果指定了timeout,并且有一个正值,而回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。

为什么须要 requestIdleCallback

在网页运行中,有很多耗时但又不是那么重要的工作。这些工作和重要的工作如对用户的输出作出及时响应的之类的工作,它们共享事件队列。如果两者发生冲突,用户体验会很蹩脚。

requestIdleCallback 就解决了这个痛点,requestIdleCallback 会在每一帧完结时并且有闲暇工夫执行回调。

假如须要大量波及到 DOM 的操作的计算,在运算时,浏览器可能就会呈现显著的卡顿行为,甚至不能进行任何操作,因为是 JS 单线程,就算用在输出解决,给定帧渲染和合成之后,用户的主线程就会变得闲暇,直到下一帧的开始。

而这些闲暇工夫能够拿来解决低优先级的工作,React16 的调度策略异步可中断,其中要害就靠的这个(polyfill)办法性能;React 把工作细分(工夫切片),在浏览器闲暇的工夫去执行,从而尽可能地进步渲染性能。

工夫切片的实质是模仿实现 requestIdleCallback

讲到这里,从 React15 到 React16 Fiber,对整体性能来说是大优化了;但要晓得的是,React16 绝对 15 做出的优化,并不是大大减少了任务量,你写的代码的工作总量并没有变动,只是把闲暇工夫利用起来了,不停的干活,就能更快的把活干完;这只是其中一个角度,React 还做了辨别优先级执行等等。

requestAnimationFrame会在每次屏幕刷新的时候被调用,而requestIdleCallback则会在每次屏幕刷新时,判断当前帧是否还有多余的时间,如果有,则会调用requestAnimationFrame的回调函数

利用这个特性,我们可以在动画执行的期间,利用每帧的空闲时间来进行数据发送的操作,或者一些优先级比较低的操作,此时不会使影响到动画的性能,或者和requestAnimationFrame搭配,可以实现一些页面性能方面的的优化,

图片中是两个连续的执行帧,大致可以理解为两个帧的持续时间大概为16.67,图中黄色部分就是空闲时间。所以,requestIdleCallback中的回调函数仅会在每次屏幕刷新并且有空闲时间时才会被调用.

react 的 fiber 架构也是基于 requestIdleCallback 实现的, 并且在不支持的浏览器中提供了 polyfill

requestIdleCallback()常用来切割长任务,利用空闲时间执行,避免主线程长时间阻塞。

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 这个办法呢?

  1. 浏览器兼容不好的问题
  2. requestIdleCallback 的 FPS 只有 20,也就是 50ms 刷新一次,远远低于页面晦涩度的要求,所以 React 团队须要本人实现。

留神:timeRemaining 最大为 50ms,是有依据钻研得出的,即是说人对用户输出的 100 毫秒以内的响应通常被认为是刹时的,不会被人察觉到。将闲暇工夫限度在 50ms 内意味着即便在闲置工作开始后立刻产生用户操作,用户代理依然有残余的 50ms 能够在其中响应用户输出而不会产生用户可察觉的滞后。

requestIdleCallback 和 requestAnimationFrame 区别

  • requestAnimationFrame 的回调会在每一帧确定执行,属于高优先级工作。
  • requestIdleCallback 的回调则不肯定,有闲暇工夫才执行,属于低优先级工作。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注