status
type
tags
category
slug
summary
date
finished_date
icon
password
背景知识
帧
Frame
动画或视频的本质是先画出一张张图片,然后快速地切换图片。每一张以快速播放时,我们的大脑会将图像识别为运动状态。
一个完整的画面即是一帧。
帧率
Frame Rate
屏幕每秒的画面数,以 fps (Frames Per Second) 为单位。
帧率越高,动画或视频就越流畅。
刷新率
Refresh Rate
屏幕每秒重新的图像次数,以 Hz 为单位。
显卡输出的帧数可以无限高,而刷新率会受到显示器硬件的制约。
对于大部分非 G-SYNC 技术的显示器来说,刷新率都是恒定的,属于显示器的出厂属性,常见的有 60 Hz、144 Hz。
- G-SYNC 技术在显示器中内置一枚可与 GeForce 硬件直接通讯的芯片,这枚自带缓存的芯片可以协调显示器与 GPU outputbuffer 之间的数据同步。
- GeForce 显卡是 NVIDIA(英伟达)的核心产品系列之一。
如果屏幕的刷新率为 60Hz,而显卡的输出高于 60 fps,两者不同步,画面便会撕裂 (Screen Tearing),即显示器把两帧或更多帧同时显示在同一画面上的一个现象。
因此浏览器每一帧画面的渲染最好要在 内完成。超出这个时间,页面就会出现卡顿、丢帧 (Frame Dropping)。我们写代码时也应力求不让一帧的工作量超过 。
- 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps 的话,那么浏览器就会选择 30fps 的更新速率。
常见 API
setInterval
如果在 JS 中需要定时重复某件事,以前只有一种方法可以实现:
setInterval()
。但是它有个缺陷:和显示器的刷新频率无法对应。
比如说显示器每 刷新一次,
setInterval
函数设置的间隔执行也是 ,可是我们无法确保频率刷新的时候, setInterval
的操作正好被执行,所以会出现动画效果生硬不连贯等情况。例子
以下是一段代码,模拟使用
setInterval
来检测游览器每一帧的间隔时间,并输出每一帧的间隔时间和总帧数。我们可以看到打印结果中,每帧绘制的时间并不是稳定在 左右,导致最后的总帧数也少于 帧。
了解过 JS 异步函数的朋友都知道setInterval
属于宏任务,那么它设定的回调函数会在主线程上其他所有同步代码执行完毕,才插到任务队列等待执行。
JS 在并发编程上一个重要特点是 “Run To Completion”。即在事件循环的一次 Tick 中, 如果要执行的逻辑太多,则会一直阻塞下一个 Tick,所有异步过程都会被阻塞。
如果在某个时刻有太多 JS 要执行,导致绘制的帧所需时间超过了 ,它会跳过当前帧的绘制,直接进入下一次事件循环的执行阶段。
- 控制台会出现警告
⚠️ warning: [Violation] ‘setInterval’ handler took XXXms
requestAnimationFrame
rAF
现在有了一个更好的代替方案:
requestAnimationFrame(
callback
)
。它接受一个回调函数,首次调用后,并让函数递归调用。当浏览器的显示频率刷新的时候,此函数会被执行,即只有完成上一帧才能加载下一帧(速度会随着计算负载的升高而减慢)。且页面不在当前标签页时,它会暂停,从而节省资源。
- 这里实际不是真正意义上的递归。该 API 不是在调用自身,而是在请求被调用。每次执行完毕,都可以回收那部分 RAM。
- 如果函数简单地调用自己,那将是一个无限递归。在这种情况下,堆栈会爆炸。
例子
但我们可以看到打印结果中,每帧绘制的时间几乎稳定在 左右,最后的总帧数也接近 180 帧。
requestAnimationFrame
代码在渲染和绘画事件运行。它能精准卡住显示器刷新的时间,对于60HZ 显示器对应 执行一次,对于 120HZ 显示器对应 执行一次,使渲染不易卡顿。
rAF 既不属于宏任务,又不属于微任务。
它在浏览器渲染,在微任务执行执行。
进阶 API
requestIdleCallback
rIC
游览器的渲染过程通常可以使用上图表示(不同引擎内核有差异),rAF上文已经提到,而结尾发现了一个陌生的
requestIdleCallback(
callback
)
方法。它是一个用于在执行后台和低优先级任务的 API。这对于处理那些不紧急的任务非常有用,如日志记录、预加载数据等。
但是假如浏览器一直处于忙碌的状态,
requestIdleCallback
的任务可能永远不会执行。例子
场景一不使用 requestIdleCallback
场景二引入 requestIdleCallback
前者明显疯狂掉帧
React 的 Fiber 架构也是基于
requestIdleCallback
实现的, 并在不支持的浏览器中提供了 Polyfill。- 感兴趣的朋友可以阅读:这可能是最通俗的 React Fiber(时间分片) 打开方式
requestVideoFrameCallback
rVFC
它是一个用于在更新时执行回调函数的 API。这对于开发者在每一帧视频呈现之前执行一些操作非常有用,例如同步动画、处理视频帧数据等。
例子
场景一不使用 requestVideoFrameCallback
场景二引入 requestVideoFrameCallback
(代码其实几乎一模一样,只不过调用的 API 名字改变了)
怎么优化后反而 “掉帧” 了?
requestAnimationFrame
是用于让帧率与浏览器的刷新率相匹配,它的回调函数可能在没有新的视频帧的时候也被调用。
requestVideoFrameCallback
是专门为视频处理设计的,它的回调函数只会在有新的视频帧的时候被调用,从而减少没必要的渲染。
在开发 awesome-hands-control 这个项目的过程中,有位网友给我提了个 PR,借助
requestVideoFrameCallback
操作虚拟相机 (virtual camera)。