requestAnimationFrame和setTimeout有什么不同

前置基础:理解 requestAnimationFrame 的必备认知

在学习 requestAnimationFrame 之前,需要先建立两个底层认知,理解动画流畅性的本质与传统方案的缺陷。

1. 显示器刷新率与浏览器的 "帧"

  • 刷新率:显示器每秒更新画面的次数,单位为赫兹(Hz)。主流显示器为 60Hz,即每秒刷新 60 次画面;高刷屏可达 120Hz、144Hz。每两次刷新的间隔称为一帧,60Hz 下单帧时间预算约为 16.67 毫秒。
  • 浏览器的一帧:浏览器完成一次完整画面更新的完整流程,包含 JavaScript 执行、样式计算、布局、绘制、合成等步骤,最终输出到显示器。动画流畅的核心,就是每帧都按时完成所有工作,不超时、不丢帧。

2. 传统定时器动画的核心痛点

早期前端动画普遍通过 setTimeoutsetInterval 定时修改元素样式实现,存在两个原生缺陷:

  • 时机不同步:定时器属于事件循环中的宏任务(浏览器任务队列中的异步任务,按顺序等待执行),执行时机与浏览器渲染周期无关,回调可能落在一帧的任意时刻,容易导致画面丢帧、卡顿。
  • 资源无节制:即使页面被切换到后台、标签页不可见,定时器依然会持续执行,浪费 CPU 资源与设备电量。

requestAnimationFrame 正是为解决动画的时机同步与性能优化问题而诞生的原生 API。


一、requestAnimationFrame 核心概念

1. 定义

requestAnimationFrame(行业内常简称 rAF )是 HTML5 标准定义的浏览器原生 API,用于向浏览器注册一个回调函数,要求浏览器在下一次屏幕重绘之前执行该回调,是专门面向视觉动画优化的调度接口。通俗解释:它相当于给浏览器发送一个 "渲染前执行" 的预约,让动画代码和屏幕的刷新节奏天然对齐,从机制上避免不同步导致的卡顿。

2. 底层原理

rAF 的核心本质是与浏览器渲染流水线深度绑定的回调调度机制,核心逻辑有三点:

  1. 刷新率自动对齐:浏览器会根据当前显示器的刷新率,自动调度回调的执行频率,保证每帧最多执行一次回调,不会出现多余的计算。
  2. 精准执行时机:回调固定在每帧的「JS 任务执行完毕、样式计算开始之前」执行,此时修改元素样式,会在当前帧完成布局与绘制,不会出现跨帧延迟。
  3. 后台自动休眠:当页面标签页被隐藏、最小化时,浏览器会自动暂停 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. 底层原理

浏览器完成一帧画面的标准流水线,按执行顺序分为以下阶段:

  1. 处理用户输入事件(点击、滚动、键盘输入等)
  2. 执行宏任务与微任务(即 JavaScript 事件循环中的任务队列)
  3. 执行 requestAnimationFrame 回调队列
  4. 样式计算(Style):计算所有元素最终生效的 CSS 样式
  5. 布局(Layout,俗称回流):计算元素的位置与尺寸
  6. 绘制(Paint,俗称重绘):将元素的视觉内容绘制到对应图层
  7. 合成(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. 应用场景

  • 长时间运行的页面动画,自动优化后台资源占用
  • 对时长精度要求高的动画,通过时间戳精准控制进度
  • 可交互的动画组件,支持随时暂停、重置与终止

五、常见认知误区

  1. 误区:requestAnimationFrame 一定是 16.6ms 执行一次

    纠正:执行间隔由显示器刷新率决定。60Hz 屏幕约 16.6ms,120Hz 高刷屏约 8.3ms;若主线程被长任务阻塞,帧间隔会进一步拉长,rAF 会自动推迟执行,并非固定时间间隔。

  2. 误区:用了 requestAnimationFrame 动画就一定流畅

    纠正:rAF 只解决了执行时机同步的问题。如果回调内部包含大量计算、频繁触发重排重绘,总工作量超过单帧时间预算,动画依然会卡顿。流畅动画的核心是控制单帧内的总工作量,而非单纯使用 rAF。

  3. 误区:requestAnimationFrame 只能用来做动画

    纠正:rAF 的本质是「下一帧渲染前执行」,除动画外,还可用于大批量 DOM 分片渲染、避免强制同步布局、拆分长任务等场景,所有需要和渲染节奏对齐的逻辑都可以使用。

  4. 误区: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 事件,页面隐藏时主动暂停逻辑,进一步优化资源占用。
  • 高刷屏场景下,若不需要过高帧率,可通过时间戳做帧节流,将动画控制在目标帧率,降低性能消耗。
相关推荐
Jackson__1 小时前
问到你想清楚,这个 skill 专治没想明白就写代码!
前端·github·ai编程
এ慕ོ冬℘゜1 小时前
前端实战:Textarea 实时字数计数输入器(带字数限制+UI优化完整源码)
前端·ui
程序员黑豆2 小时前
从零开始:创建第一个鸿蒙应用程序
前端·harmonyos
只与明月听3 小时前
LangChain 学习-掌握LangChain Core API
前端·人工智能·后端
Listen·Rain3 小时前
Vue3中组件间通信详解
前端·javascript·vue.js
Listen·Rain3 小时前
Vue3中customRef详解
前端·javascript·vue.js
香芋芋圆3 小时前
国内常用坐标系全解析与WebGIS框架实战指南(Cesium/Mapbox/Leaflet避坑手册)
前端·javascript
小粉粉hhh3 小时前
React(三)——Redux
前端·javascript·react.js
易知微EasyV数据可视化4 小时前
定制化3D组件开发:打造差异化孪生体验
前端·人工智能·经验分享·数字孪生