requestAnimationFrame:实现更流畅的前端动画

浏览器需要不断执行 JavaScript、计算样式、布局页面并绘制画面。常见设备的刷新率是 60Hz,也就是一帧大约只有 16.67ms。如果某一帧的工作时间太长,动画就会出现卡顿。

以前写简单动画时,比较容易想到 setInterval 或 setTimeout。但它们的执行时间和浏览器绘制时机并不一定同步,回调可能在一帧的中间或末尾执行,最终造成丢帧。

requestAnimationFrame 会在浏览器准备绘制下一帧之前调用回调,更适合实现连续动画。

一个简单的动画示例

下面的代码让元素在一秒内向右移动 300 像素:

const box = document.querySelector('.box')
let startTime

function animate(timestamp) {
if (!startTime) startTime = timestamp

const progress = Math.min((timestamp - startTime) / 1000, 1)
box.style.transform = `translateX(${progress * 300}px)`

if (progress < 1) {
requestAnimationFrame(animate)
}
}

requestAnimationFrame(animate)

浏览器会把当前时间传给回调函数,我们可以根据开始时间计算动画进度,而不是假设每次回调之间间隔完全相同。

为什么不直接使用定时器

setTimeout 和 setInterval 只能表示“过一段时间执行”,并不能保证回调正好发生在浏览器绘制之前。当页面比较忙时,定时器还可能被延迟执行。

requestAnimationFrame 的优势主要有两点:

  • 与浏览器刷新节奏同步
  • 页面不在当前标签页时,浏览器通常会暂停或降低调用频率,减少不必要的计算

减少布局和重绘成本

动画本身流畅,还需要注意修改的 CSS 属性。通常可以优先使用 transform 和 opacity,避免频繁修改会触发布局的属性,例如宽度、高度和部分位置属性。

.box {
transition: transform 200ms ease;
}

如果只是简单的位移、缩放或透明度变化,CSS 动画通常已经足够;需要根据实时数据计算运动轨迹时,再考虑使用 requestAnimationFrame。

动画优化的目标不是把所有逻辑都塞进 requestAnimationFrame,而是让每一帧做的事情足够少,并且和浏览器的绘制节奏配合起来。