scroll事件滚动卡顿是因为高频触发且回调中含重操作,RAF节流可精准对齐帧率,仅在下一帧前执行一次更新,配合transform和虚拟列表进一步优化性能。

scroll 事件为什么一滚就卡?
因为浏览器在快速滚动时,scroll 事件可能每秒触发上百次,而每次回调若包含 DOM 查询、getBoundingClientRect()、样式计算或 innerHTML 更新,就极易超出 16.6ms 的帧预算。更糟的是,传统 setTimeout 节流无法与屏幕刷新同步——它可能在帧中间执行,导致跳帧或重复渲染。
用 requestAnimationFrame 实现真·60FPS 节流
这不是“加个定时器”那么简单。requestAnimationFrame(简称 RAF)天然绑定浏览器渲染周期,确保你的处理逻辑只在下一帧绘制前运行一次,且后台标签页自动暂停,不浪费资源。
关键做法是:把实际的业务逻辑(比如计算可视区域、更新 DOM)延迟到 RAF 回调中执行,而 scroll 回调本身只做最轻量的事——记录最新滚动位置并标记“需要更新”:
- 用一个布尔标志
isQueued防止重复注册 RAF -
scroll回调里只更新scrollTop或scrollLeft缓存,并调用requestAnimationFrame(update) - 真正耗时的计算和 DOM 操作全部放在
update函数里
let isQueued = false;
let currentScrollTop = 0;
function update() {
isQueued = false;
// ✅ 这里才做真实工作:计算可视索引、diff 渲染、setTransform 等
renderVisibleItems(currentScrollTop);
}
window.addEventListener('scroll', () => {
currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (!isQueued) {
isQueued = true;
requestAnimationFrame(update);
}
});
节流间隔设成 16ms 还是交给 RAF?
别手动设 16 或 16.7。RAF 本身已按设备刷新率调度(60Hz/90Hz/120Hz),硬编码固定值反而会破坏适配。例如在 120Hz 屏幕上,RAF 自然以 ~8.3ms 执行,你却卡死在 16ms,等于主动丢帧。
常见错误包括:
- 在 RAF 回调里又嵌套
setTimeout(..., 16)—— 多余且错乱 - 用
throttle(fn, 16)包裹 RAF 调用 —— 两层节流互相干扰 - 在 RAF 中修改
style.left或style.top—— 触发强制同步布局,直接干掉帧率
配合 transform 和 will-change 提升合成效率
即使节流到位,如果每次更新都写 element.style.top = y + 'px',浏览器仍要反复重排(reflow)。正确做法是:
- 用
transform: translateY(y + 'px')替代 top/left —— GPU 加速,不触发重排 - 对滚动容器加
style.willChange = 'transform'(仅对频繁动画元素启用,避免滥用) - 确保滚动容器有独立合成层(例如加
transform: translateZ(0)或 opacity: 0.999)
复杂点在于:RAF 节流解决的是“执行频率”,但最终是否掉帧,还取决于单次执行是否超时。所以 renderVisibleItems 内部也得轻量化——比如用虚拟列表只更新 20 个可见项,而不是遍历 10 万条数据。

















