requestAnimationFrame 和 setTimeout 的区别
设计目的与基本概念
-
requestAnimationFrame 是浏览器专门为高性能动画设计的 API,其核心目标是与浏览器渲染周期同步,实现流畅的视觉效果。
- 当调用 requestAnimationFrame 时,实际上是告诉浏览器:"我希望在下一次重绘之前执行这个回调函数来更新动画"。
- 这种方法确保动画更新与屏幕刷新节奏完美同步,通常以 60Hz(约 16.7ms/帧)的频率运行
- requestAnimationFrame 是声明式的("请在下次绘制前调用我")
-
setTimeou t则是 JavaScript 中通用的延时执行工具,用于在指定的毫秒数后执行一次回调函数(setTimeout)或重复执行(setInterval)。
- 它不关心浏览器的渲染周期,只是简单地将任务推入异步队列等待执行。
- 例如,setTimeout(callback, 16)试图近似 60FPS 的动画效果,但无法保证精确的时间控制。
- setTimeout 是命令式的("在 16 毫秒后调用我,不管系统状态如何")
事件循环与调度机制
-
setTimeout 的执行依赖于 JavaScript 的事件循环机制。
-
当调用 setTimeout(callback, delay)时,计时器开始计时,但回调函数不会在计时结束后立即执行,而是被放入任务队列。只有当调用栈为空时,事件循环才会从任务队列中取出这个回调执行。 这意味着:
- 实际执行时间可能远晚于预期:如果主线程有长时间运行的同步代码,setTimeout 回调必须等待,导致动画卡顿。
- 时间间隔不精确:即使设置为 16.7ms(60FPS),由于系统负载和事件循环的不可预测性,实际间隔可能在 16.7-100ms 甚至更长。
- 累积误差:连续的 setTimeout 调用会因执行延迟而产生误差积累,导致动画越来越不同步。
-
相比之下,requestAnimationFrame 的回调由浏览器渲染管线直接调度,在"渲染前"阶段执行。浏览器会自动将多个 requestAnimationFrame 回调合并到同一帧处理, 确保:
- 与显示器刷新同步:60Hz 屏幕约 16.7ms 一帧,120Hz 屏幕约 8.3ms 一帧,自动适配。
- 避免过度渲染:即使代码频繁调用,浏览器也只会按屏幕刷新率执行,不会产生多余的重绘。
- 高精度时间戳:回调函数接收一个 DOMHighResTimeStamp 参数,精确表示触发时间(精度可达 1ms),便于实现基于时间的动画计算
后台标签页行为
当页面切换到后台或最小化时,两者的表现截然不同:
- setTimeout/setInterval 会继续执行,消耗 CPU 资源,尽管这些更新对用户不可见。
- requestAnimationFrame 会自动暂停,直到页面再次可见。 这不仅节省资源,还能在恢复时从断点继续,避免"跳帧"。这种差异在移动设备上尤为明显。 使用 setTimeout 的动画在后台运行时,会不必要地消耗电量,可能触发浏览器的节流策略,导致返回页面时动画异常
CPU/GPU 效率
requestAnimationFrame 的设计充分考虑了浏览器渲染优化:
- 批量 DOM 操作:同一帧内的所有 DOM 修改会被浏览器智能合并,减少重排(Reflow)和重绘(Repaint)次数。
- 空闲期优化:浏览器可以在帧之间的空闲期执行垃圾回收等任务,避免干扰动画。
- 硬件加速:现代浏览器会对 requestAnimationFrame 动画应用 GPU 加速,特别是 CSS 变换和 Canvas 操作。
而 setTimeout 动画容易引发布局抖动(Layout Thrashing)
- 频繁的样式读写迫使浏览器提前计算布局,导致性能下降。
测试表明,相同的动画效果,requestAnimationFrame 的 CPU 占用率通常比 setTimeout 低 2-3 倍。
帧率稳定性
通过 FPS(Frames Per Second)测试可以直观看到差异:
- requestAnimationFrame:稳定在 60FPS(或屏幕刷新率),波动范围小。
- setTimeout:FPS 波动大,尤其在系统负载高时可能出现明显卡顿,即使设置为 16.7ms 间隔,实际可能只有 30-45FPS。
适用场景对比
requestAnimationFrame 最适合:
- 连续动画:CSS 变换、Canvas 绘图、WebGL 渲染等需要流畅视觉效果的情况。
- 滚动相关操作:无限滚动、视差滚动、懒加载等,避免滚动事件的高频触发导致的性能问题。
- 游戏开发:游戏主循环需要稳定的帧率,与渲染同步。
- 大数据渲染:分批次将大量 DOM 元素插入文档,避免界面冻结
setTimeout/setInterval 更适合:
- 非视觉任务:延迟提示、轮询服务器、超时控制等。
- 一次性延迟:如弹窗延迟显示、工具提示延迟隐藏等。
- 不精确的重复任务:如每 5 分钟检查一次新邮件
验证执行逻辑
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>requestAnimation 和 setTimeout 的区别</h2>
</body>
<script>
const runFunc = (fn, isRequestAnimationFrame = true) => {
if (isRequestAnimationFrame) {
/**
* requestAnimationFrame
*
* 如果页面在后台运行时,那么将不会执行
* 等回到前台,才会继续执行
*
*
* 打印 166ms
*
* 打印 200ms
*
* 后台不执行
*
* 回到前台
* 打印 12000ms
*/
requestAnimationFrame(fn);
} else {
/***
* setTimeout
*
* 虽然规定的是 16.66ms 执行一次
* 但是如果页面在后台运行,那么不会 16.66ms 执行一次
* 浏览器会做优化,会在 1000ms 左右执行一次
*
* 打印 166ms
*
* 打印 200ms
*
* 后台
*
* 打印 1200ms
* 打印 2200ms
* 打印 3200ms
* 打印 4200ms
* 打印 5200ms
* 打印 6200ms
* xxxx 直到终止
*
*/
setTimeout(fn, 16.66);
}
};
let startTime = null;
function animate(timestamp) {
console.log(new Date());
const newTimestamp = timestamp || document.timeline.currentTime || performance.now();
if (!startTime) startTime = newTimestamp;
const progress = newTimestamp - startTime;
console.log('🚀 ~ animate ~ progress:', progress);
if (progress < 10000) {
// 运行 2 秒
// requestAnimationFrame(animate);
runFunc(animate, false);
}
}
// requestAnimationFrame(animate);
runFunc(animate, false);
</script>
</html>