打造 60 fps 丝滑页面:从 requestAnimationFrame 到 requestVideoFrameCallback
发布于: 2024/06/08
字数: 0
status
type
category
tags
slug
date
summary
icon

背景知识

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 来检测游览器每一帧的间隔时间,并输出每一帧的间隔时间和总帧数。
notion image
我们可以看到打印结果中,每帧绘制的时间并不是稳定在 左右,导致最后的总帧数也少于 帧。

了解过 JS 异步函数的朋友都知道 setInterval 属于宏任务,那么它设定的回调函数会在主线程上其他所有同步代码执行完毕,才插到任务队列等待执行。
notion image
notion image
JS 在并发编程上一个重要特点是 “Run To Completion”。即在事件循环的一次 Tick 中, 如果要执行的逻辑太多,则会一直阻塞下一个 Tick,所有异步过程都会被阻塞。
如果在某个时刻有太多 JS 要执行,导致绘制的帧所需时间超过了 ,它会跳过当前帧的绘制,直接进入下一次事件循环的执行阶段。
  • 控制台会出现警告 ⚠️ warning: [Violation] ‘setInterval’ handler took XXXms
notion image
notion image

requestAnimationFrame

rAF
现在有了一个更好的代替方案: requestAnimationFrame(callback) 。
它接受一个回调函数,首次调用后,并让函数递归调用当浏览器的显示频率刷新的时候,此函数会被执行,即只有完成上一帧才能加载下一帧(速度会随着计算负载的升高而减慢)。且页面不在当前标签页时,它会暂停,从而节省资源。
  • 如果函数简单地调用自己,那将是一个无限递归。在这种情况下,堆栈会爆炸。

例子

notion image
但我们可以看到打印结果中,每帧绘制的时间几乎稳定在 左右,最后的总帧数也接近 180 帧。
requestAnimationFrame 代码在渲染和绘画事件运行。它能精准卡住显示器刷新的时间,对于60HZ 显示器对应 执行一次,对于 120HZ 显示器对应 执行一次,使渲染不易卡顿。
notion image
notion image
💡
rAF 既不属于宏任务,又不属于微任务。
它在浏览器渲染,在微任务执行执行。

进阶 API

requestIdleCallback

rIC
notion image
游览器的渲染过程通常可以使用上图表示(不同引擎内核有差异),rAF上文已经提到,而结尾发现了一个陌生的 requestIdleCallback(callback) 方法。
它是一个用于在执行后台和低优先级任务的 API。这对于处理那些不紧急的任务非常有用,如日志记录、预加载数据等。
notion image
但是假如浏览器一直处于忙碌的状态,requestIdleCallback 的任务可能永远不会执行。

例子

场景一不使用 requestIdleCallback
最终的打印结果
最终的打印结果
场景二引入 requestIdleCallback
最终的打印结果
最终的打印结果
前者明显疯狂掉帧

React 的 Fiber 架构也是基于 requestIdleCallback  实现的, 并在不支持的浏览器中提供了 Polyfill。
React 的 Fiber 架构也是基于 requestIdleCallback 实现的, 并在不支持的浏览器中提供了 Polyfill。

requestVideoFrameCallback

rVFC
它是一个用于在更新时执行回调函数的 API。这对于开发者在每一帧视频呈现之前执行一些操作非常有用,例如同步动画、处理视频帧数据等。

例子

场景一不使用 requestVideoFrameCallback
最终的打印结果
最终的打印结果
场景二引入 requestVideoFrameCallback
(代码其实几乎一模一样,只不过调用的 API 名字改变了)
最终的打印结果
最终的打印结果
怎么优化后反而 “掉帧” 了?
  • requestAnimationFrame  是用于让帧率与浏览器的刷新率相匹配,它的回调函数可能在没有新的视频帧的时候也被调用。
  • requestVideoFrameCallback 是专门为视频处理设计的,它的回调函数只会在有新的视频帧的时候被调用,从而减少没必要的渲染

在开发 awesome-hands-control  这个项目的过程中,有位网友给我提了个 PR,借助 requestVideoFrameCallback  操作虚拟相机 (virtual camera)。
在开发 awesome-hands-control 这个项目的过程中,有位网友给我提了个 PR,借助 requestVideoFrameCallback 操作虚拟相机 (virtual camera)

参考材料

 
2023 - 2026