JavaScript 动画核心是每帧更新元素状态并以 60fps 重绘,requestAnimationFrame 因与浏览器渲染机制协同、对齐重绘时机、自适应刷新率而比 setInterval 更流畅。

用 JavaScript 实现动画效果的核心,是 ** 在每一帧中更新元素的视觉状态(比如位置、透明度、缩放等),并让 浏览器 以接近 60fps 的节奏 重绘**。而 requestAnimationFrame(简称 rAF)之所以比 setInterval 更流畅,根本原因在于它 ** 与浏览器的渲染机制深度协同 **,而不是靠固定时间间隔“硬塞”更新。
动画的本质:不是“定时执行”,而是“跟帧走”
浏览器渲染页面是一个周期性流程:接收输入 → 运行 JS → 计算样式和布局 → 绘制图层 → 合成显示。这个流程每秒尝试执行约 60 次(即 16.67ms/ 帧)。理想动画,就是每次都在“绘制前”完成状态更新,确保每一帧都呈现最新画面。
-
setInterval(fn, 16)只是“尽量每 16ms 调用一次 fn”,但 JS 执行可能卡顿、任务队列堆积、甚至跨帧执行,导致多次更新挤在单帧里,或一帧内漏掉更新 —— 表现为掉帧、抖动、延迟感。 -
requestAnimationFrame(fn)则把回调交给浏览器调度:浏览器保证在 ** 下一次重绘之前 ** 执行该函数,且自动根据设备刷新率调整(如 iPad Pro 120Hz 下约 8.3ms/ 帧),天然对齐渲染节奏。
用 requestAnimationFrame 写一个基础位移动画
以下是一个平滑移动
从左到右的示例:
const box = document.getElementById('box'); let position = 0; const endPosition = 400; function animate() { if (position < endPosition) {position += 2; // 每次移动 2px box.style.transform = `translateX(${position}px)`; requestAnimationFrame(animate); // 下一帧继续 } } requestAnimationFrame(animate); // 启动
- 不依赖
setTimeout或setInterval控制节奏,全由浏览器决定何时调用下一次animate。 - 即使主线程短暂繁忙,rAF 会自动跳过某些帧(而非累积延迟),避免“追赶式”卡顿。
- 页面切到后台时,大多数浏览器会暂停 rAF,节省资源;而
setInterval仍运行,造成无效计算。
为什么 setInterval 容易“假流畅”
表面看 setInterval(fn, 16) 数值上接近 60fps,但实际存在多个隐性问题:
立即学习“Java 免费学习笔记(深入)”;
- 时间精度不可靠:JS 计时器最小粒度通常为 4ms(HTML5 规范限制),且受系统负载影响,16ms 往往变成 18ms、22ms 甚至更长。
- 与重绘脱节:你在第 15ms 更新了样式,但浏览器可能刚完成上一帧绘制,要等到第 32ms 才开始下一帧 —— 中间 17ms 白等,用户看到的是延迟生效。
- 无法动态适应:高刷屏(120Hz)、低性能设备(30fps)、省电模式下,
setInterval仍固执地按 16ms 跑,要么浪费资源,要么超负荷掉帧。
补充建议:让动画更专业
- 优先使用 CSS 动画 /
transform+opacity属性,它们可被 GPU 加速,rAF 只负责控制逻辑(如触发、暂停、插值)。 - 需要精确时间控制时(如倒计时 + 动画同步),可用
performance.now()记录起始时间,在 rAF 回调中计算已过去毫秒数,再算出当前进度 —— 而非依赖帧数累加。 - 避免在 rAF 回调中做重排(如读取
offsetTop)或大量 DOM 操作,防止强制同步布局,拖慢帧率。