实现60FPS丝滑滚动需用requestAnimationFrame(rAF)对齐浏览器刷新节奏,配合节流闭包防重复调度;禁用debounce/普通throttle,避免延迟或掉帧;rAF内须避免重排重绘操作,推荐transform/opacity动画及读写分离;启用passive事件提升原生滚动响应。

要实现 60FPS 的丝滑滚动,核心不是单纯“节流”,而是把滚动处理逻辑精准对齐到浏览器的刷新节奏上。节流闭包 + requestAnimationFrame(rAF)的组合,本质是用 rAF 做调度器、用闭包维持状态,避免高频触发导致的丢帧和卡顿。
为什么不用 debounce 或普通 throttle?
debounce 会延迟响应,滚动中松手才执行,失去实时反馈;普通 throttle(如 Lodash 的)按固定时间间隔执行,可能和屏幕刷新不同步(比如 16.6ms 刚好错开),仍会掉帧。而 rAF 天然绑定每帧(约 16.6ms),且会在浏览器下一次重绘前执行,保证视觉更新不跳帧。
节流闭包的作用:防重复调度
rAF 本身不防抖——如果滚动持续触发,反复调用 requestAnimationFrame(fn),可能产生多个待执行帧任务,造成冗余或竞态。闭包用来标记“当前帧已调度”,确保同一帧内只执行一次:
const scrollHandler = () => {
let isScheduled = false;
return () => {
if (isScheduled) return;
isScheduled = true;
requestAnimationFrame(() => {
// ? 实际要做的工作(如更新 sticky、视差、懒加载)
updateOnScroll();
isScheduled = false; // 重置,为下一帧准备
});
};
};
const handleScroll = scrollHandler();
window.addEventListener('scroll', handleScroll);
关键细节:别在 rAF 里做重操作
即使对齐了帧率,若 updateOnScroll() 内部有以下行为,依然会卡顿:
- 频繁读取
offsetTop、getBoundingClientRect()(触发布局重排) - 直接修改大量 DOM 样式(尤其影响布局的属性如
width、height、top) - 未用
will-change或transform启用硬件加速
✅ 推荐做法:用 transform + opacity 做动画;把读取操作(get)和写入操作(set)分离,或用 ResizeObserver / IntersectionObserver 替代部分滚动监听。
进阶:配合 passive event 提升原生滚动体验
在支持的浏览器中,给 scroll 事件添加 { passive: true },可让浏览器默认不等待 JS 执行就滚动,大幅提升触控/滚轮响应速度:
window.addEventListener('scroll', handleScroll, { passive: true });
⚠️ 注意:此时不能在 handler 中调用 preventDefault()(如自定义滚动拦截),否则会被忽略。











