前置基础:理解 requestAnimationFrame 的必备认知
在学习 requestAnimationFrame 之前,需要先建立两个底层认知,理解动画流畅性的本质与传统方案的缺陷。
1. 显示器刷新率与浏览器的 "帧"
- 刷新率:显示器每秒更新画面的次数,单位为赫兹(Hz)。主流显示器为 60Hz,即每秒刷新 60 次画面;高刷屏可达 120Hz、144Hz。每两次刷新的间隔称为一帧,60Hz 下单帧时间预算约为 16.67 毫秒。
- 浏览器的一帧:浏览器完成一次完整画面更新的完整流程,包含 JavaScript 执行、样式计算、布局、绘制、合成等步骤,最终输出到显示器。动画流畅的核心,就是每帧都按时完成所有工作,不超时、不丢帧。
2. 传统定时器动画的核心痛点
早期前端动画普遍通过 setTimeout 或 setInterval 定时修改元素样式实现,存在两个原生缺陷:
- 时机不同步:定时器属于事件循环中的宏任务(浏览器任务队列中的异步任务,按顺序等待执行),执行时机与浏览器渲染周期无关,回调可能落在一帧的任意时刻,容易导致画面丢帧、卡顿。
- 资源无节制:即使页面被切换到后台、标签页不可见,定时器依然会持续执行,浪费 CPU 资源与设备电量。
requestAnimationFrame 正是为解决动画的时机同步与性能优化问题而诞生的原生 API。
一、requestAnimationFrame 核心概念
1. 定义
requestAnimationFrame(行业内常简称 rAF )是 HTML5 标准定义的浏览器原生 API,用于向浏览器注册一个回调函数,要求浏览器在下一次屏幕重绘之前执行该回调,是专门面向视觉动画优化的调度接口。通俗解释:它相当于给浏览器发送一个 "渲染前执行" 的预约,让动画代码和屏幕的刷新节奏天然对齐,从机制上避免不同步导致的卡顿。
2. 底层原理
rAF 的核心本质是与浏览器渲染流水线深度绑定的回调调度机制,核心逻辑有三点:
- 刷新率自动对齐:浏览器会根据当前显示器的刷新率,自动调度回调的执行频率,保证每帧最多执行一次回调,不会出现多余的计算。
- 精准执行时机:回调固定在每帧的「JS 任务执行完毕、样式计算开始之前」执行,此时修改元素样式,会在当前帧完成布局与绘制,不会出现跨帧延迟。
- 后台自动休眠:当页面标签页被隐藏、最小化时,浏览器会自动暂停 rAF 回调的调度;页面重新可见时自动恢复,以此降低性能消耗。
3. 具象示例
下面是用 rAF 实现元素水平匀速移动的标准写法,也是行业通用的最佳实践:
javascript
const target = document.querySelector('.animate-box');
const totalDistance = 300; // 总移动距离 300px
const totalDuration = 2000; // 动画总时长 2000ms
let startTimestamp = null; // 动画起始时间戳
// 每帧执行的动画函数
function step(currentTimestamp) {
// 首次执行时记录起始时间
if (!startTimestamp) startTimestamp = currentTimestamp;
// 计算已运行时长与动画进度
const elapsed = currentTimestamp - startTimestamp;
const progress = Math.min(elapsed / totalDuration, 1); // 进度限制在 0~1
// 更新元素位置
target.style.transform = `translateX(${progress * totalDistance}px)`;
// 进度未完成则继续注册下一帧回调
if (progress < 1) {
requestAnimationFrame(step);
}
}
// 启动动画
requestAnimationFrame(step);
该写法的核心特点是:通过时间差计算动画进度,而非固定每帧移动固定像素。即使设备帧率波动,动画总时长也保持一致,不会出现慢设备上动画变慢的问题。
4. 应用场景
- DOM 元素的自定义动画、过渡效果,尤其是需要精细控制进度的动画
- Canvas、SVG、WebGL 等图形绘制类动画
- 页面滚动联动效果、视差滚动、滚动平滑处理
- 大批量 DOM 的分片渲染,利用每帧空闲时间分批更新页面
二、requestAnimationFrame 的执行时序
1. 定义
执行时序指 rAF 回调在浏览器单帧渲染流水线中所处的具体阶段,以及它和其他异步任务的先后执行顺序。
2. 底层原理
浏览器完成一帧画面的标准流水线,按执行顺序分为以下阶段:
- 处理用户输入事件(点击、滚动、键盘输入等)
- 执行宏任务与微任务(即 JavaScript 事件循环中的任务队列)
- 执行 requestAnimationFrame 回调队列
- 样式计算(Style):计算所有元素最终生效的 CSS 样式
- 布局(Layout,俗称回流):计算元素的位置与尺寸
- 绘制(Paint,俗称重绘):将元素的视觉内容绘制到对应图层
- 合成(Composite):将多个图层合并为最终画面,输出到显示器
rAF 回调恰好位于「JS 常规任务之后、样式计算之前」,这个时机修改样式,会被当前帧的布局、绘制流程直接处理,是修改视觉样式的最佳时机。
3. 具象示例
对比定时器与 rAF 修改样式的生效时机差异:
javascript
运行
ini
// 写法 A:setTimeout 修改样式
setTimeout(() => {
target.style.width = '200px';
// 该修改大概率错过当前帧,需要等到下一帧才会渲染到屏幕
}, 0);
// 写法 B:rAF 修改样式
requestAnimationFrame(() => {
target.style.width = '200px';
// 该修改会在当前帧的布局阶段被计算,同帧即可渲染生效
});
setTimeout(fn, 0) 会将回调放入下一轮宏任务队列,执行时机晚于当前帧的渲染流程;而 rAF 回调在当前帧渲染前执行,样式修改可以立刻被本次渲染流水线处理。
4. 应用场景
- 需要精准控制样式生效时机的同步动画
- 读取 DOM 尺寸后立即修改样式的场景,避免强制同步布局
- 多元素、多动画协同更新,保证所有变化在同一帧内生效
三、与传统定时器动画的选型对比
1. 定义
setTimeout / setInterval 是通用型定时任务 API,设计目标是延迟执行任务;requestAnimationFrame 是专属渲染优化的 API,设计目标是流畅的视觉更新。两者底层调度机制完全不同,适用场景有明确边界。
2. 底层原理
- 定时器动画:基于事件循环的宏任务调度,回调进入任务队列等待主线程空闲执行。主线程繁忙时回调会被推迟,执行时机与渲染周期无关联,容易出现一帧内执行多次回调、或多帧才执行一次回调的情况,导致动画卡顿跳帧。
- rAF 动画:直接挂载到渲染流水线,由浏览器统一调度。每帧内无论注册多少次回调,都会集中在 rAF 阶段依次执行,不会出现回调堆积;主线程阻塞时自动降低帧率,保证画面更新的连贯性。
3. 具象对比
表格
| 对比维度 | setTimeout / setInterval | requestAnimationFrame |
|---|---|---|
| 执行时机 | 事件循环调度,与渲染周期无关 | 绑定渲染周期,每帧重绘前固定执行 |
| 帧率稳定性 | 不稳定,易掉帧、跳帧 | 稳定,自动对齐显示器刷新率 |
| 后台页面行为 | 持续执行,占用系统资源 | 自动暂停,页面恢复后自动继续 |
| 时间精度 | 毫秒级,存在最小 4ms 延迟限制 | 微秒级高精度时间戳,无额外延迟 |
| 回调堆积 | 高频调用易产生回调堆积 | 单帧内多次调用合并执行,无堆积 |
| 设计定位 | 通用异步定时任务 | 专门面向视觉动画与渲染优化 |
4. 应用场景
- 优先选用 rAF:所有视觉动画、画面更新相关逻辑,尤其是持续运行的动画、平滑过渡效果
- 优先选用定时器:非视觉类定时任务,比如数据轮询、业务逻辑延迟执行、与渲染无关的定时操作
四、requestAnimationFrame 的核心特性
1. 定义
rAF 自带三项原生特性,是其成为动画首选方案的核心优势:后台自动休眠、高精度时间戳、可取消机制。
2. 底层原理
- 后台自动休眠:浏览器通过页面可见性 API(Page Visibility API)检测标签页状态。页面隐藏时,停止调度 rAF 回调,将帧率降为 0;页面重新可见时,自动恢复调度,大幅降低后台页面的性能开销。
- 高精度时间戳 :rAF 回调会自动传入一个
DOMHighResTimeStamp类型的参数,表示回调被触发的精确时间。该时间从页面加载开始计时,精度可达微秒级,不受系统时间修改影响,是计算动画进度的标准依据。 - 可取消机制 :调用
requestAnimationFrame会返回一个非零整数 ID,通过cancelAnimationFrame(id)可以取消对应的待执行回调,逻辑与clearTimeout一致,用于主动终止动画。
3. 具象示例
标准的动画启停封装写法:
javascript
class AnimationController {
constructor() {
this.animationId = null;
}
start(animateFn) {
const step = (timestamp) => {
animateFn(timestamp);
this.animationId = requestAnimationFrame(step);
};
this.animationId = requestAnimationFrame(step);
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
}
在组件卸载、弹窗关闭等场景下调用 stop() 方法,可以彻底清理回调,避免内存泄漏与无效执行。
4. 应用场景
- 长时间运行的页面动画,自动优化后台资源占用
- 对时长精度要求高的动画,通过时间戳精准控制进度
- 可交互的动画组件,支持随时暂停、重置与终止
五、常见认知误区
-
误区:requestAnimationFrame 一定是 16.6ms 执行一次
纠正:执行间隔由显示器刷新率决定。60Hz 屏幕约 16.6ms,120Hz 高刷屏约 8.3ms;若主线程被长任务阻塞,帧间隔会进一步拉长,rAF 会自动推迟执行,并非固定时间间隔。
-
误区:用了 requestAnimationFrame 动画就一定流畅
纠正:rAF 只解决了执行时机同步的问题。如果回调内部包含大量计算、频繁触发重排重绘,总工作量超过单帧时间预算,动画依然会卡顿。流畅动画的核心是控制单帧内的总工作量,而非单纯使用 rAF。
-
误区:requestAnimationFrame 只能用来做动画
纠正:rAF 的本质是「下一帧渲染前执行」,除动画外,还可用于大批量 DOM 分片渲染、避免强制同步布局、拆分长任务等场景,所有需要和渲染节奏对齐的逻辑都可以使用。
-
误区:requestAnimationFrame 和 setTimeout (fn, 0) 效果相近
纠正:两者执行时机完全不在同一层级。
setTimeout(fn, 0)是将回调放入下一轮宏任务队列,执行时机晚于当前帧渲染;rAF 回调在当前帧渲染前执行,时机远早于 setTimeout 0,且精度更高。
六、落地实操建议
基础使用规范
- 采用「时间差计算进度」的写法,不要使用固定增量。避免因设备帧率差异导致动画速度不一致,保证不同设备上动画总时长统一。
- 动画停止或组件卸载时,必须调用
cancelAnimationFrame清理回调,防止内存泄漏与无效执行。 - 动画进度计算使用回调传入的高精度时间戳,不要使用
Date.now(),后者精度更低且受系统时间影响。
性能优化建议
- 不要在 rAF 回调中执行非渲染类的重计算逻辑,耗时业务逻辑尽量放到
requestIdleCallback或 Web Worker 中处理。 - 避免在 rAF 中先读取 DOM 样式、再修改 DOM 样式,该操作会触发强制同步布局,导致性能大幅劣化。
- 同一帧内的多个动画逻辑,尽量合并到同一个 rAF 回调中处理,减少多次调度的额外开销。
边界兼容建议
- 低版本 IE 浏览器不支持 rAF,可使用
setTimeout模拟的 polyfill 做降级兼容。 - 非高优先级的动画,可监听
visibilitychange事件,页面隐藏时主动暂停逻辑,进一步优化资源占用。 - 高刷屏场景下,若不需要过高帧率,可通过时间戳做帧节流,将动画控制在目标帧率,降低性能消耗。