性能优化之requestAnimationFrame和setInterval

requestAnimationFrame 执行频率分析

requestAnimationFrame(animateScroll) 的执行频率不固定,而是与显示器刷新率同步

核心原理

requestAnimationFrame 是浏览器提供的 API,它会:

  • 与显示器刷新率同步执行回调函数
  • 通常在 60Hz 的显示器上,大约每 16.67ms 执行一次(1000ms ÷ 60 ≈ 16.67ms)
  • 在高刷新率显示器上(如 120Hz),执行频率会更高(约 8.33ms)

ScrollArea.vue 中的实现分析

在 ScrollArea.vue 中,animateScroll 函数通过递归调用 requestAnimationFrame 实现平滑滚动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const animateScroll = () => {
// 如果暂停了,则不执行动画逻辑
if (paused.value || !scrollerRef.value || !shouldScroll.value) {
return;
}

let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0');

// 向上滚动,值变小
let newY = currentY - props.speed;

// 检查是否需要"重置"位置,实现无缝循环
if (-currentY >= singleListHeight) {
newY += singleListHeight;
}

scrollerRef.value.style.transform = `translateY(${newY}px)`;

// 递归调用 requestAnimationFrame
animationFrameId = requestAnimationFrame(animateScroll);
};

滚动速度控制

虽然 requestAnimationFrame 的执行频率不固定,但组件通过 props.speed 参数控制每次执行时的滚动距离:

  • props.speed = 0.5(默认值)表示每次动画帧滚动 0.5px
  • 在 60Hz 显示器上,每秒滚动距离为:0.5px × 60 = 30px/秒
  • 可以通过调整 speed 属性来改变滚动速度

性能优势

使用 requestAnimationFrame 的好处:

  1. 节能:页面不可见时自动暂停
  2. 流畅:与显示器刷新率同步,避免掉帧
  3. 优化:浏览器会自动优化动画性能

总结

  • requestAnimationFrame 的执行频率与显示器刷新率同步
  • 通常在 60Hz 显示器上约每 16.67ms 执行一次
  • 组件通过 speed 参数控制每次滚动的距离,从而控制整体滚动速度
  • 当前默认配置下(speed=0.5),在 60Hz 显示器上的滚动速度约为 30px/秒

和setInterval对比

是否可以固定执行频率

核心结论

不能直接通过 requestAnimationFrame 固定执行频率,因为它的执行频率由浏览器和显示器刷新率决定。但可以通过以下方式实现固定频率:

方案一:使用 setInterval 替代

requestAnimationFrame 替换为 setInterval 可以实现固定频率执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 定义固定时间间隔(毫秒)
const intervalTime = 50; // 50ms = 20fps
let intervalId: number | null = null;

const animateScroll = () => {
if (paused.value || !scrollerRef.value || !shouldScroll.value) {
return;
}

// 滚动逻辑保持不变
let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0');
let newY = currentY - props.speed;

if (-currentY >= singleListHeight) {
newY += singleListHeight;
}

scrollerRef.value.style.transform = `translateY(${newY}px)`;
};

const startAnimation = () => {
stopAnimation();
if (shouldScroll.value && !paused.value) {
intervalId = setInterval(animateScroll, intervalTime);
}
};

const stopAnimation = () => {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
};

方案二:结合时间戳的混合方案

使用 requestAnimationFrame 但根据时间戳控制执行频率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let lastTime = 0;
const targetInterval = 50; // 目标间隔时间(毫秒)

const animateScroll = (timestamp: number) => {
if (paused.value || !scrollerRef.value || !shouldScroll.value) {
animationFrameId = requestAnimationFrame(animateScroll);
return;
}

// 计算时间差
const elapsed = timestamp - lastTime;

// 只有当时间差大于目标间隔时才执行滚动
if (elapsed >= targetInterval) {
let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0');
let newY = currentY - props.speed;

if (-currentY >= singleListHeight) {
newY += singleListHeight;
}

scrollerRef.value.style.transform = `translateY(${newY}px)`;
lastTime = timestamp;
}

animationFrameId = requestAnimationFrame(animateScroll);
};

两种方法对比

特性 requestAnimationFrame setInterval
执行频率 与显示器刷新率同步(60Hz≈16.67ms) 精确固定(如50ms)
性能 与浏览器渲染同步,更流畅 可能与渲染不同步,导致掉帧
节能 页面不可见时自动暂停 持续运行,消耗资源
适用场景 视觉动画、UI渲染 后台任务、精确计时

建议

  1. 视觉滚动动画:建议继续使用 requestAnimationFrame,保持与显示器同步的流畅性
  2. 需要精确计时:使用 setInterval,但注意可能的性能问题
  3. 混合方案:对于需要控制速度但又想保持流畅的场景,使用时间戳方案

性能优化之requestAnimationFrame和setInterval
https://cszy.top/20260325-性能优化之requestAnimationFrame和setInterval/
作者
csorz
发布于
2026年3月19日
许可协议