React 之从视觉暂留到 FPS、刷新率再到显卡、垂直同步再到16ms的故事

12,700 阅读8分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

视觉暂留现象

当我们观看一个物体时,物体成像于视网膜上,经由视神经传给人脑,人才能感觉到物体的像。

但当物体移去时,视神经对物体的印象不会立即消失,而要延续 0.1 - 0.4 秒的时间,人眼的这种性质被称为“眼睛的视觉暂留现象”。

我们平时看到的视频,其实是由一张张静态画面组成,“帧”指的就是每一张静态的画面。由于人眼的视觉暂留现象,我们才感觉它是动态的。

帧率

帧率(frame rate)是用于测量显示帧数的度量。测量单位为“每秒显示帧数”(frame per second),也就是我们常说的 FPS。

通常,要避免动作不流畅,FPS 最低是 30,基本流畅则需要 60。太低的时候用户会感到卡顿,太高的时候,超过一个临界值(大约 100 左右),人眼会感受不到明显的差别。

PS:FPS 在游戏领域指的是第一人称射击游戏 (First-person shooter),比如使命召唤、守望先锋、战地、光环、无主之地、孤岛惊魂、绝地求生等。

刷新率

显示器的刷新率是指显示器每秒绘制新图像的次数。其单位为赫兹 (Hz)。

赫兹(符号:Hz)则是国际单位制中频率的单位,表示每一秒周期性事件发生的次数。

如果显示器刷新率为 144Hz,指的就是显示器每秒钟会刷新图像 144 次。

FPS 与 Hz

FPS 指的是內容展示,而 Hz 用于屏幕显示。换句话说,FPS 可以无限高,但刷新率决定了展示的上限。

举个例子,如果你在 120Hz 的手机上播放 60FPS 的影片,你看起來的感觉几乎与 60Hz 的手机是一样的。

还有一点,Hz 通常为恒定速率,不会随场景复杂度而变化。而 FPS,你可以理解为,GPU 每秒平均渲染的帧数,根据渲染情况,帧与帧之间的帧时间并非恒定不变。

画面撕裂

当 FPS 与 Hz 不匹配时,无论是帧率大于刷新率还是刷新率大于帧率,都有可能会出现“画面撕裂”。

所谓画面撕裂(Screen Tearing),是指显示器把两帧或更多帧同时显示在同一画面上的一个现象,比如这种:

image.png

为了解释画面撕裂这个问题,我们需要引入“显卡”这个概念。

显卡

显卡又称显示卡( Video card),是计算机中一个重要的组成部分,承担输出显示图形的任务。

主流显卡的显示芯片主要由 NVIDIA 和 AMD 两大厂商制造,通常将采用 NVIDIA 显示芯片的显卡称为 N 卡,而将采用 AMD 显示芯片的显卡称为 A 卡。

显卡的主要芯片叫“显示芯片”(Video chipset,也叫 GPU 或 VPU,图形处理器或视觉处理器),是显卡的主要处理单元。

简单来说,显卡中,GPU 负责制作画面,图像显示器负责呈现画面。

但显示器显示画面的时候,它是读取 GPU 制作的画面,从上到下,一行一行扫描显示出来的,如果 GPU 刚好制作出图片,显示器刚好开始扫描,这个配合天衣无缝。

但是怎么能总是更刚刚好呢?有的时候战斗比较激烈,GPU 渲染的画面速度慢了一点,显示器展示完上一个画面,下一个画面还没有来,显示器就接着扫描上一个画面,结果扫描到一半,下个画面来了,于是在当前的位置接着扫描下个画面的内容,于是这个画面一部分 A,一部分 B,显示出来,就是画面撕裂的效果。

反过来,GPU 渲染的非常快,显示器还未扫描完上张图,下张图就来了,于是接着上张图的位置扫描下张图,于是也发生了图片撕裂。

垂直同步

你可能想,难道就不能控制显示器,扫描完一张再扫描下一张吗?

当然是可以的,这就是很多游戏里会提供的垂直同步功能(Vertical Synchronization):当显示器在扫描数据时,会等到完全扫描数据后,再扫描 GPU 提交的新数据。

但是呢,这样做就慢了。依然以战斗激烈的时候为例,GPU 渲染的画面速度慢了一点,显示器展示完上一个画面,下一个画面还没有来,那就接着扫描上一个画面,结果扫描到一半,下个画面来了,但是开了垂直同步,于是显示器把上个画面扫描完,再开始扫描下个画面,这就造成了多余的一帧。这对于竞技游戏还是有点影响的。

电影的24帧

我们前面讲了:要避免动作不流畅,FPS 最低是 30,基本流畅则需要 60。

但我们也知道,电影最常见的帧数就是每秒 24 帧,为什么电影 24 帧就很流畅,游戏 24 FPS 就会显得卡顿呢?

最主要的是两个原因,一个是两者画面生成方式不同。

电影的拍摄,它是在每一秒内拍摄一定数量的照片,24 帧就是 24 张照片,每张照片的拍摄都会有一定的曝光时间,这样拍摄出来的图片,如果有高速移动的物体,它会出现模糊,这就跟人眼观察高速物体时的感觉相似,所以人们看电影时感觉自然流畅。如果你去截电影中的图片,尤其是有大幅度动作的场面,截出的图往往都会有模糊。

但是游戏不一样,它的每一帧都很清晰,如果帧数低,它就会让用户感觉是跳来跳去,如果帧数高,虽然每一帧都是清晰的,但因为有视觉暂留,前一张图片暂留,与下一张图片重叠,在大脑看来也会变得模糊,这就接近了自然状态。当然现在的很多游戏,也加入了动态模糊效果,就是让画面看起来更加自然一些。

另外一点就是,电影的帧率是稳定的,游戏则不一定。游戏并不是一秒有 60 帧就不卡,举个极端的例子,如果前 0.5 秒放了 59 帧,后 0.5 秒放了 1 帧,用户同样会感到卡顿。所以如果有比如 0.1 秒内没有稳定输出帧数,用户可能就会感到卡顿。而且游戏会涉及到用户的操作,如果用户做了动作,但帧没有跟上,也很容易感受到卡顿。

浏览器 60Hz?

聊了那么多,开始进入本篇的正题。

如果我们去看 React 的 Concurrent Mode、Fiber、Time Slicing 等知识点,我们一定会听过 16ms 这个说法,大致是浏览器的刷新频率是 60Hz,每帧就是 1000ms / 60Hz = 16.6ms,所以 JavaScript 的操作要在 16.6ms内完成,否则用户就会感到卡顿。

这其中有几个不算很准确的说法。

首先 16ms 这样的说法,是来自于显示器刷新频率为 60Hz,浏览器只是一个应用而已,它控制不了硬件的刷新率,它能做的就是不断的提供每帧的图像,如果提供的慢,跟不上刷新率,就会卡顿。而之所以是 60Hz,不是因为我们的显示器只是 60Hz,而是对于人眼来说,比 60Hz 更高我们可能不会有非常明显的感觉,但如果比 60Hz 低,我们感受到的就比较明显了。

而对于一帧来说,如果要保证流畅性,浏览器确实要在 16.6ms 内完成输出一个画面,但这个时间并不都是留给 JavaScript 的,我们学习浏览器渲染原理的时候,可能会看到这张图: image.png 你可以看到,在一帧里,浏览器要处理用户输入事件、执行脚本、处理窗口变更、滚动、媒体查询、动画、执行 requestAnimationFrame 和 Intersection-Observer 回调、布局计算、绘制等,留给 JS 执行的时间并不多,在 React 中默认的时间切片时间也只有 5ms

好了,正题结束。下篇讲讲跟帧相关的两个 API: requestAnimationFrame 和 requestIdleCallBack。

React 系列

  1. React 之 createElement 源码解读
  2. React 之元素与组件的区别
  3. React 之 Refs 的使用和 forwardRef 的源码解读
  4. React 之 Context 的变迁与背后实现
  5. React 之 Race Condition
  6. React 之 Suspense

React 系列的预热系列,带大家从源码的角度深入理解 React 的各个 API 和执行过程,全目录不知道多少篇,预计写个 50 篇吧。