requestanimationframe 比 settimeout 更适合 canvas 动画,因其与屏幕刷新同步、自动节流、标签页隐藏时暂停;需配合 cancelanimationframe 防泄漏,draw 开头用 clearrect 清屏,且分离 update 与 render 逻辑。

requestAnimationFrame 为什么比 setTimeout 更适合 canvas 动画
因为浏览器能主动调度帧率,与屏幕刷新同步(通常是 60fps),避免丢帧或卡顿;而 setTimeout 依赖固定延时,实际执行受 JS 主线程阻塞影响,容易漂移甚至掉到 30fps 以下。尤其在 canvas 频繁重绘时,requestAnimationFrame 会自动在空闲时段回调,还能在标签页不可见时暂停调用——这点对电池和性能很关键。
常见错误现象:setTimeout(() => draw(), 16) 看似模拟 60fps,但实际每帧耗时若超过 16ms,就会累积延迟,动画越来越慢;更糟的是,切到其他 tab 后它还在跑,白耗 CPU。
基础逐帧循环结构必须包含 cancelAnimationFrame 防止内存泄漏
手动启动动画后,如果不显式终止,即使 canvas 元素已被移除或页面跳转,回调仍可能持续触发,导致对象无法回收、定时器堆积。
实操建议:
- 用一个变量保存
requestAnimationFrame返回的 ID,例如let animationId = null - 每次递归调用前先清上一帧:
if (animationId) cancelAnimationFrame(animationId) - 在页面卸载或组件销毁时(如
beforeunload、React 的useEffect cleanup)调用cancelAnimationFrame(animationId) - 不要直接写成
requestAnimationFrame(loop)无限递归而不存 ID —— 这样你根本没法停
canvas 清屏逻辑必须放在 draw 函数开头,且优先用 clearRect 而非 fillRect
逐帧动画的核心是“先清后画”,否则上一帧残留会叠加出拖影。很多人误用 ctx.fillRect(0, 0, width, height) 填充背景色来清屏,这在有透明度或复杂背景时会出问题(比如盖掉 canvas 的 CSS 背景图);而 ctx.clearRect(0, 0, width, height) 是真正擦除像素,语义准确、性能略优。
使用场景注意点:
- 如果 canvas 本身有
background-image或需要保留某层底图,就别清全屏,只清动画区域:clearRect(x, y, w, h) - 清屏前务必确认 canvas 的
width和height属性已设好(不是 CSS 样式),否则clearRect可能清错范围 - 不要在每次 draw 里重复获取
getContext('2d'),缓存一次即可
动画状态更新与绘制必须分离,避免逻辑耦合导致帧率不稳
把数据更新(如小球 x += vx)和像素绘制(ctx.beginPath(); ctx.arc(...))混在同一函数里,会让性能分析困难;一旦绘制变慢,运动逻辑也会被拖慢,造成“越卡越偏”的恶性循环。
推荐结构:
- 定义独立的
update()函数:只做数值计算、碰撞检测、状态切换 - 定义独立的
render()函数:只调用 canvas API 绘制,不修改任何状态 - 主循环中按顺序执行:
update(); render(); requestAnimationFrame(loop) - 如需插值或时间校准(应对帧率波动),在
update()中接收上一帧时间戳参数,用deltaTime = now - lastTime计算位移
容易踩的坑:直接在 render() 里改位置变量,结果调试时加个 console.log 就让动画变慢,还找不到原因。
canvas 逐帧动画真正的复杂点不在“怎么动”,而在“动得准不准”——时间戳处理、清除边界、状态生命周期管理,这些地方松一扣,动画就飘了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











