写前端动画做到一定阶段,几乎都会撞上 requestAnimationFrame。网上一搜,教程一大堆,但绝大多数只是让你“背下来这样写”,至于它为什么存在、浏览器到底在哪个环节调用它、为什么有时候用 setInterval 会卡、为什么切后台动画就停了,很少有人一次讲清楚。这篇文章不搞虚的,直接从浏览器渲染机制开始,把 requestAnimationFrame 的来龙去脉、运行规律、封装思路和排错经验一次说透。不管你是刚入门前端的新手,还是写过不少动效但心里没底的同学,这篇都能让你看完之后,在真正写代码时更有底气。
1. 先把“卡顿”的根源挖出来:为什么 setTimeout 做动画不靠谱
1.1 一次浏览器渲染周期里,到底发生了什么
要理解 requestAnimationFrame,得先理解浏览器是“一帧一帧”画页面的。每次你更新了元素的样式,浏览器并不会立刻把结果画到屏幕上,而是等下一个合适的时机,统一走一遍完整的通道。这个通道大概包含这几个阶段:执行 JavaScript、计算样式(Style)、生成布局(Layout)、绘制图形(Paint),最后把所有图层合成在一起显示出来(Composite)。这一整套在 60Hz 刷新率的屏幕上,目标是在 16.7ms 内完成一次;在 120Hz 的屏幕上,这个时间预算会被压缩到大约 8.3ms。
这里有个关键点:这些阶段是串行的,而且每一帧的起点和终点之间有一条严格的时间线。如果在某个阶段卡住了,比如 JavaScript 同步计算跑了 80ms,那么后面的样式计算、布局、绘制全部都要往后排队,用户看到的就是掉帧、卡顿。所以判断一段动画代码好不好,核心指标不是“写得快不快”,而是它有没有挤占和破坏这一帧的完整节奏。
1.2 setInterval 做动画的三个硬伤
在早期没有 requestAnimationFrame 的时候,大家喜欢用 setInterval(fn, 1000 / 60) 来做动画,也就是自己模拟 60fps 的调用节奏。但现实很快打了脸,因为这种模拟至少有三个解决不了的问题。
第一个问题是定时器触发时机和屏幕刷新时机经常错位。就算你设定 16.7ms 执行一次,浏览器实际的定时器精度受任务队列影响很大,回调可能落在 14ms、20ms 甚至更离散的位置。假设回调执行时刚好错过了本轮渲染的“样式计算和合成窗口”,那么效果就会顺延到下一帧,表现出来就是动画抖动、不流畅。
第二个问题是后台标签页会被浏览器强制节流。浏览器为了省电,会降低后台页面里定时器的执行频率,setInterval 的回调可能从 16ms 一次被拉到 1000ms 一次。对动画来说这倒不算太大的灾难,因为用户没在看;但对一些依赖定时器做轮询或倒计时的业务来说,体验就是灾难。
第三个问题是定时器没法感知“当前这一帧到底要不要渲染”。就算当前页面元素完全没变化、或者用户已经把滚动条拖到底了,定时器依然会傻乎乎地在后台执行。而浏览器真正关心的是“画面需要刷新时,你要更新什么状态”,requestAnimationFrame 就是这种思路的产物——浏览器自己掌握渲染节奏,每次准备重绘前,主动叫你一声。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. requestAnimationFrame 的运行机制,越早明白越少踩坑
2.1 基础 API:注册、回调参数、取消循环
先看最基本的用法。requestAnimationFrame 接收一个回调函数,浏览器会在下一次屏幕重绘之前调用这个回调,并且只调用一次。如果你需要持续动画,就得在回调里面再次调一次 requestAnimationFrame:
js复制let rafId = 0;
function loop(now) {
// 这里更新元素状态
update(now);
// 继续注册下一帧
rafId = requestAnimationFrame(loop);
}
// 启动
rafId = requestAnimationFrame(loop);
请注意这里的 now 参数。回调函数能接收到一个 DOMHighResTimeStamp 类型的时间戳,单位是毫秒,精度比
