如何高效节流requestAnimationFrame?帧率节流省电最佳实践
针对高刷显示器下requestAnimationFrame回调频率远高于动画实际需求的问题(比如144Hz显示器下回调次数是必要次数的4倍),以下是几种成熟的节流方案,同时解答你对“空回调浪费资源”的疑问:
1. 基于timestamp的条件执行(最通用方案)
这是你见过的主流方案,核心是记录上一次执行动画逻辑的时间戳,每次回调时判断当前时间与上次的差值是否达到目标间隔(比如60fps对应16.67ms,30fps对应33.33ms),仅满足条件时才执行动画更新,否则直接调度下一次回调。
示例代码:
let lastRenderTime = 0; const targetFPS = 60; const targetInterval = 1000 / targetFPS; function animate(timestamp) { if (timestamp - lastRenderTime < targetInterval) { requestAnimationFrame(animate); return; } // 这里写实际的动画更新逻辑 updateAnimationState(); lastRenderTime = timestamp; requestAnimationFrame(animate); } requestAnimationFrame(animate);
关于你担心的“空回调浪费资源”:这种方式的开销远低于直觉。requestAnimationFrame的回调是在浏览器渲染帧的空闲阶段触发的,空回调只是做一次简单的时间差计算和函数调用,几乎不占用CPU资源——这和死循环完全不同,死循环会持续霸占CPU时间片,而空回调会快速退出,让浏览器处理其他任务。
2. 定时器配合requestAnimationFrame(进一步降低调度开销)
如果想减少不必要的回调触发,可以用setTimeout控制requestAnimationFrame的注册频率,只在需要更新时才让浏览器调度回调:
const targetFPS = 60; const targetInterval = 1000 / targetFPS; function animate() { // 执行动画逻辑 updateAnimationState(); // 延迟指定时间后再注册下一次回调 setTimeout(() => { requestAnimationFrame(animate); }, targetInterval); } requestAnimationFrame(animate);
这种方式的优势是不会在不需要更新的帧里触发requestAnimationFrame回调,进一步降低浏览器的调度成本。但要注意,定时器精度可能受主线程繁忙程度影响,动画平滑度可能略有下降。
3. 适配显示器刷新率的动态节流
你提到的“指定目标刷新率需结合显示器实际情况”非常合理。比如显示器是60Hz时,即使设置50Hz目标,浏览器最好以60Hz的整数分频(比如30Hz)调度,避免画面撕裂或卡顿。基于timestamp的方案可以轻松实现这一点:将目标间隔设为显示器刷新率间隔的整数倍即可。
可以通过浏览器API获取刷新率,动态计算最优间隔:
// 获取显示器刷新率(部分现代浏览器支持) const screenRefreshRate = window.screen.refreshRate || 60; // 比如用刷新率的1/2来节流(60Hz变30Hz,144Hz变72Hz) const throttleRatio = 2; const targetInterval = 1000 / (screenRefreshRate / throttleRatio);
这样既能节省电量,又能保证动画和显示器刷新率同步,避免不必要的卡顿。
内容的提问来源于stack exchange,提问作者David Easley

