4、动画基础概念:帧率(FPS)、时间步长(Delta Time)、动画循环(Game Loop)模式
做动画,说白了就是让画面动起来。但怎么动得流畅、动得自然,这里头有不少门道。我刚开始写动画的时候,以为只要在循环里不断重绘就行,结果跑出来的效果要么卡成幻灯片,要么快得像开了倍速。后来才明白,核心就三个东西:帧率、时间步长、动画循环。
这一节,咱们就把这三个概念彻底搞清楚。你想想看,搞懂了它们,后面做字体动画、刷新联动,才能心里有底。
4.1 帧率(FPS)—— 动画的“心跳”
帧率,全称 Frames Per Second,每秒显示的画面数。单位是 fps。60fps 就是一秒闪过 60 张图。
人眼有个特点:当画面切换速度超过每秒 10-12 帧,就会觉得是连续运动。电影是 24fps,电视是 30fps,电脑显示器主流是 60fps。为什么是 60?因为 60fps 的刷新间隔约 16.67ms,人眼基本感觉不到闪烁了。
关键认知: 帧率不是越高越好,而是越稳定越好。30fps 稳定,体验远好于 60fps 忽高忽低。
我在项目中遇到过一个问题:一个粒子动画在开发机上跑 120fps,非常丝滑。结果部署到用户的老笔记本上,直接掉到 15fps,动画像在爬。这就是典型的“帧率敏感”问题。所以,永远不要假设用户的设备跟你一样快。
| 帧率 | 每帧间隔 | 典型场景 | 体验评价 |
|---|---|---|---|
| 24 fps | 41.67 ms | 电影 | 可接受,有轻微抖动 |
| 30 fps | 33.33 ms | 电视、低配游戏 | 流畅,适合交互 |
| 60 fps | 16.67 ms | 显示器、手机 | 非常流畅,标准目标 |
| 120 fps | 8.33 ms | 高端电竞屏 | 极致丝滑,但功耗高 |
我的习惯: 开发时我会在屏幕上显示一个实时 FPS 计数器。一旦发现 FPS 低于 50,我就知道该优化了。别等到用户投诉才去查。
4.2 时间步长(Delta Time)—— 动画的“尺子”
光有帧率还不够。假设你让一个小球每帧移动 10 像素。60fps 时,小球一秒移动 600 像素。但如果掉到 30fps,一秒只移动 300 像素。同样的代码,在不同设备上速度不一样,这显然不合理。
解决办法就是引入 Delta Time(通常写作 dt)。它记录的是上一帧到这一帧实际经过的时间,单位是秒。
有了 dt,移动逻辑就变成:位置 += 速度 × dt。这样不管帧率怎么变,小球一秒移动的距离都是固定的。
// 错误写法:依赖帧率
x += 10; // 60fps 时一秒 600px,30fps 时一秒 300px
// 正确写法:依赖时间
x += 10 * dt; // 无论帧率多少,一秒都是 600px
嗯,这里要注意:dt 的单位要统一。我见过有人用毫秒做单位,结果动画快了 1000 倍,画面直接飞出去。建议统一用秒。
我曾经踩过的坑: 在页面切后台再切回来时,dt 会变得非常大(比如切后台 10 秒,dt = 10)。如果不做限制,动画会瞬间跳到一个离谱的位置。解决方案是给 dt 设一个上限,比如最大 0.1 秒。
4.3 动画循环(Game Loop)—— 动画的“引擎”
有了帧率和 dt,还需要一个循环来驱动它们。这就是动画循环,也叫 Game Loop。它负责三件事:输入 → 更新 → 渲染。
在浏览器里,最标准的动画循环接口是 requestAnimationFrame(简称 rAF)。它告诉浏览器:“下次重绘之前,请调用我这个函数。” 浏览器会尽量保持 60fps 的节奏,而且页面切到后台时会自动暂停,省电又省资源。
function gameLoop(timestamp) {
// 1. 计算 dt
const dt = timestamp - lastTimestamp;
lastTimestamp = timestamp;
// 2. 更新逻辑(位置、状态、碰撞等)
update(dt);
// 3. 渲染画面
render();
// 4. 请求下一帧
requestAnimationFrame(gameLoop);
}
let lastTimestamp = 0;
requestAnimationFrame(gameLoop);
这里有三种常见的循环模式,我整理了一张表,方便你对比:
| 模式 | 核心思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定时间步长 | 每次更新固定 dt(如 16.67ms),渲染按实际帧率 | 物理模拟稳定,不依赖帧率 | 实现稍复杂,可能多次更新才渲染一次 | 物理引擎、游戏 |
| 可变时间步长 | 每次更新使用实际 dt | 实现简单,直接 | dt 波动大时物理不稳定 | UI 动画、简单效果 |
| 半固定时间步长 | 固定 dt 但允许累积,超过上限则丢弃 | 兼顾稳定性和性能 | 需要处理累积逻辑 | 大多数实战项目 |
我个人习惯用半固定时间步长。说白了,就是给 dt 设个上限,防止切后台回来时动画“瞬移”。同时内部用固定步长做物理更新,保证碰撞检测不出错。
4.4 知识体系总览
下面这张图把三个概念的关系串起来了。你可以看到,帧率是节奏,dt 是标尺,循环是引擎。三者缺一不可。
4.5 实战建议:如何选择循环模式
你可能会问:那我到底用哪种?
我的建议很简单:
- 做 UI 动画、字体渐变、简单位移 → 用可变时间步长。代码少,够用。
- 做物理模拟、碰撞检测、游戏 → 用固定时间步长。物理稳定比什么都重要。
- 做复杂项目,不确定用户设备 → 用半固定时间步长。这是最稳妥的选择。
一个小技巧: 在开发阶段,我会在循环里加一个 console.log(dt),观察 dt 的波动。如果波动超过 5ms,说明当前帧有性能瓶颈,该去查查是哪段代码拖慢了。
好了,帧率、时间步长、动画循环这三个概念,到这里就讲透了。你想想看,有了 dt 这把“尺子”,动画就不再依赖设备帧率;有了 rAF 这个“引擎”,动画就能跟浏览器刷新同步。后面做字体显示与动画刷新联动,这些就是地基。