滚动事件应优先使用节流而非防抖,因其需持续采样状态变化;节流函数需支持leading和trailing选项,并配合passive: true提升性能。

页面滚动事件(scroll)天生高频,用户轻轻一滚,几十毫秒内可能触发十几次。如果回调里有 DOM 查询、计算可视区域、发请求或重排重绘,主线程立刻吃紧,FPS 下跌、滚动卡顿、内存堆积就全来了。直接绑 `addEventListener('scroll', handler)` 是性能雷区,必须配合节流或防抖——但 scroll 场景几乎只该用节流,防抖在这里多数会丢失关键响应。
为什么 scroll 优先选节流,而不是防抖
滚动是持续行为,你关心的是“过程中”的状态变化,比如:实时判断某个元素是否进入视口、动态更新吸顶导航、做滚动进度条。这些都需要稳定频率的采样,不能等用户“停了再算”。防抖会把中间所有滚动都丢掉,直到最后静止才执行一次,导致临界位置漏判、动画断层、交互延迟明显。
- 防抖典型适用场景:搜索框输入、窗口 resize 结束后重新布局
- 节流典型适用场景:scroll、mousemove、拖拽位置更新、Canvas 动画同步
- 一句话记牢:滚动要“匀速反馈”,不是“等它停下”
节流函数怎么写才真正可靠
简单时间戳版容易漏掉最后一次触发;纯定时器版又可能首次不执行。生产环境推荐带 leading 和 trailing 控制的增强版:
function throttle(func, wait, options = {}) {
let timeout = null;
let previous = 0;
const { leading = true, trailing = true } = options;
<p>return function(...args) {
const now = Date.now();
if (!previous && !leading) previous = now;
const remaining = wait - (now - previous);</p><pre class="brush:php;toolbar:false;">if (remaining wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(this, args);
previous = now;
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
func.apply(this, args);
previous = Date.now();
timeout = null;
}, remaining);
}
}; }
- leading: true → 第一次滚动立即执行(避免首屏无响应)
- trailing: true → 最后一次触发后补调一次(防止滚动停在边界却没触发)
- 别把 event 对象直接存闭包里——滚动事件对象会被复用,节流后拿到的可能是过期引用
比节流还重要的底层优化:passive: true
现代浏览器对未声明 passive: true 的 scroll 监听器,会强制同步阻塞滚动主线程——哪怕你的回调里只有一行 console.log。这是比节流更硬核的开关:
window.addEventListener('scroll', throttle(handleScroll, 16), {
passive: true
});
- 加了
passive: true,浏览器就知道“你不会调preventDefault()”,就能开启异步滚动优化 - 如果确实需要阻止默认滚动(比如自定义滚动容器),改用
{ passive: false },但这时务必搭配节流,否则性能雪崩 - 不加这个,节流效果打五折;加了,节流才能发挥全部价值
实际使用中的关键细节
节流只是工具,用对位置和参数才起作用:
- 检测视口变化时,别每帧都查
getBoundingClientRect(),先节流,再在节流回调里做最小必要计算 - 滚动加载更多,建议节流间隔设为 100–200ms,太长会延迟加载,太短失去节流意义
- 避免在节流回调里频繁修改样式或触发 layout,能用 CSS will-change 或 transform 就不用 top/left
- 用
requestIdleCallback做非紧急任务(如上报、预加载),别挤占滚动主线程











