Canvas 动画基础:requestAnimationFrame、时间步长与简单性能优化

一旦开始在 Canvas 上做动画,很多问题就会集中暴露出来:用什么驱动帧率、如何让不同机器上动画速度一致、如何避免每一帧都重绘整个世界。
这一篇围绕三件事展开:requestAnimationFrame 的用法、基于时间的动画设计,以及在不引入复杂框架的前提下能做的几类简单性能优化。

在最早的示例中,动画经常用:

1setInterval(draw, 16); // 约 60fps

但这有几个问题:

  • setInterval 不知道当前浏览器的刷新节奏;
  • 页面不在前台时仍可能继续执行(浪费资源);
  • 不会与浏览器的渲染管线对齐,容易出现撕裂或掉帧感。

requestAnimationFrame 的设计目标就是:

  • 让浏览器在每一帧渲染前调用你提供的回调;
  • 当页面不可见时自动降频或暂停;
  • 与显示器刷新率更好对齐。

使用方式通常是:

1function loop(timestamp) {
2  // 更新状态 + 绘制
3  requestAnimationFrame(loop);
4}
5
6requestAnimationFrame(loop);

timestamp 参数是浏览器提供的高精度时间戳(单位毫秒),可用于计算每帧的时间步长。

如果每一帧都简单地「位置加 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);

这样,无论帧率如何变化,只要时间差正确计算,动画的「物理速度」都是一致的。

在动画循环里,通常会做三件事:

  1. 清除上一帧的内容;
  2. 更新状态;
  3. 根据新状态重新绘制当前帧。

最简单的清屏方式是:

1ctx.clearRect(0, 0, canvas.width, canvas.height);

但在复杂场景里,全画布清屏 + 全量重绘会有一定开销。
如果需要做更细致的优化,可以考虑:

  • 脏矩形(dirty rectangle)策略
    • 只清除和重绘发生变化的区域;
    • 适合屏幕上大部分区域是静态的场景。
  • 分层绘制
    • 把背景、网格等静态内容绘制到离屏 Canvas 或单独一层;
    • 动态内容每帧只更新自己的层。

在实践中,可以从全量重绘开始,当性能成为瓶颈时,再逐步引入这些优化。

离屏 Canvas 可以通过:

  • document.createElement('canvas') 创建一个不插入 DOM 的画布;
  • 在支持的环境中,可使用 OffscreenCanvas

常见用途包括:

  • 预渲染复杂静态背景、图案或缓慢变化的内容;
  • 为重复使用的图形(图标、纹理)生成缓存图像。

在主画布绘制时,只需要用 drawImage 把离屏 Canvas 内容画上来,避免每一帧重复构造复杂路径或图案。

有些信息不需要每帧更新:

  • 文本统计(字数、FPS 显示);
  • 某些慢速动画或状态指示器。

可以通过简单的节流策略降低更新频率:

  • 使用累积时间差,在超过某个阈值后才更新某些内容;
  • 或者每隔 N 帧再更新一次非关键部分。

这样可以把性能预算更多留给真正需要高频刷新的图形元素。

在 Canvas 动画里,感知性能通常来自:

  • 视觉上是否流畅;
  • 是否出现明显卡顿或掉帧;
  • CPU/内存占用是否在可接受范围。

可以结合:

  • 简单的 FPS 统计(基于 timestamp 计算);
  • 浏览器 Task/Performance 面板查看帧时间;
  • 对典型场景(大数据量、多元素)做压力测试。

如果一段动画看起来不顺畅:

  • 首先用日志或简单计数检查是否在某些情况下做了过多工作;
  • 再用 DevTools 分析是哪一部分占用时间最长,是计算、绘制还是布局。

这一篇可以压缩为几个实践关键点:

  • requestAnimationFrame 驱动动画,让时间轴和浏览器渲染对齐;
  • 使用基于时间的状态更新方式,让动画速度和帧率解耦;
  • 从全量清屏 + 重绘开始,在需要时逐步引入脏矩形、分层和离屏 Canvas;
  • 对不同元素控制更新频率,把更多预算留给关键视觉部分。

在这套基础上,再叠加交互、复杂绘制与物理模拟,Canvas 动画就能在保证正确性的前提下,逐步向高性能演进。