Canvas 动画基础:requestAnimationFrame、时间步长与简单性能优化
一旦开始在 Canvas 上做动画,很多问题就会集中暴露出来:用什么驱动帧率、如何让不同机器上动画速度一致、如何避免每一帧都重绘整个世界。
这一篇围绕三件事展开:requestAnimationFrame的用法、基于时间的动画设计,以及在不引入复杂框架的前提下能做的几类简单性能优化。
1. 为什么用 requestAnimationFrame 而不是 setInterval?
在最早的示例中,动画经常用:
1setInterval(draw, 16); // 约 60fps
但这有几个问题:
setInterval不知道当前浏览器的刷新节奏;- 页面不在前台时仍可能继续执行(浪费资源);
- 不会与浏览器的渲染管线对齐,容易出现撕裂或掉帧感。
requestAnimationFrame 的设计目标就是:
- 让浏览器在每一帧渲染前调用你提供的回调;
- 当页面不可见时自动降频或暂停;
- 与显示器刷新率更好对齐。
使用方式通常是:
1function loop(timestamp) {
2 // 更新状态 + 绘制
3 requestAnimationFrame(loop);
4}
5
6requestAnimationFrame(loop);
timestamp 参数是浏览器提供的高精度时间戳(单位毫秒),可用于计算每帧的时间步长。
2. 基于时间的动画设计:让速度和帧率解耦
如果每一帧都简单地「位置加 1 像素」,在不同刷新率或不同性能的设备上,会出现:
- 帧率高的设备动画更快;
- 帧率低的设备动画更慢。
更稳妥的方式是:
- 使用「速度 × 时间」来更新状态:
- 每个对象有一个速度(例如像素/秒);
- 每帧根据「距离上次帧的时间差」来计算本次应移动的距离。
示意结构:
1let lastTime = 0;
2const speed = 100; // px per second
3let x = 0;
4
5function loop(timestamp) {
6 if (!lastTime) lastTime = timestamp;
7 const delta = (timestamp - lastTime) / 1000; // 秒
8 lastTime = timestamp;
9
10 x += speed * delta;
11
12 // 清屏 + 绘制
13
14 requestAnimationFrame(loop);
15}
16
17requestAnimationFrame(loop);
这样,无论帧率如何变化,只要时间差正确计算,动画的「物理速度」都是一致的。
3. 清屏与重绘:不要每一帧都画「无用像素」
在动画循环里,通常会做三件事:
- 清除上一帧的内容;
- 更新状态;
- 根据新状态重新绘制当前帧。
最简单的清屏方式是:
1ctx.clearRect(0, 0, canvas.width, canvas.height);
但在复杂场景里,全画布清屏 + 全量重绘会有一定开销。
如果需要做更细致的优化,可以考虑:
- 脏矩形(dirty rectangle)策略
- 只清除和重绘发生变化的区域;
- 适合屏幕上大部分区域是静态的场景。
- 分层绘制
- 把背景、网格等静态内容绘制到离屏 Canvas 或单独一层;
- 动态内容每帧只更新自己的层。
在实践中,可以从全量重绘开始,当性能成为瓶颈时,再逐步引入这些优化。
4. 离屏 Canvas:把「不必每帧计算的东西」提前算好
离屏 Canvas 可以通过:
document.createElement('canvas')创建一个不插入 DOM 的画布;- 在支持的环境中,可使用
OffscreenCanvas。
常见用途包括:
- 预渲染复杂静态背景、图案或缓慢变化的内容;
- 为重复使用的图形(图标、纹理)生成缓存图像。
在主画布绘制时,只需要用 drawImage 把离屏 Canvas 内容画上来,避免每一帧重复构造复杂路径或图案。
5. 控制更新频率:不一定所有东西都要 60fps
有些信息不需要每帧更新:
- 文本统计(字数、FPS 显示);
- 某些慢速动画或状态指示器。
可以通过简单的节流策略降低更新频率:
- 使用累积时间差,在超过某个阈值后才更新某些内容;
- 或者每隔 N 帧再更新一次非关键部分。
这样可以把性能预算更多留给真正需要高频刷新的图形元素。
6. Debug 与测量:用眼睛 + 工具双重验证
在 Canvas 动画里,感知性能通常来自:
- 视觉上是否流畅;
- 是否出现明显卡顿或掉帧;
- CPU/内存占用是否在可接受范围。
可以结合:
- 简单的 FPS 统计(基于
timestamp计算); - 浏览器 Task/Performance 面板查看帧时间;
- 对典型场景(大数据量、多元素)做压力测试。
如果一段动画看起来不顺畅:
- 首先用日志或简单计数检查是否在某些情况下做了过多工作;
- 再用 DevTools 分析是哪一部分占用时间最长,是计算、绘制还是布局。
7. 小结:先让动画「正确」,再让动画「更快」
这一篇可以压缩为几个实践关键点:
- 用
requestAnimationFrame驱动动画,让时间轴和浏览器渲染对齐; - 使用基于时间的状态更新方式,让动画速度和帧率解耦;
- 从全量清屏 + 重绘开始,在需要时逐步引入脏矩形、分层和离屏 Canvas;
- 对不同元素控制更新频率,把更多预算留给关键视觉部分。
在这套基础上,再叠加交互、复杂绘制与物理模拟,Canvas 动画就能在保证正确性的前提下,逐步向高性能演进。