滚动事件需节流以避免性能问题,推荐时间戳版(轻量)和定时器版(支持trailing补发),并配合passive、状态锁及监听器清理。

滚动事件特别容易高频触发,尤其在触屏设备或惯性滚动时,每秒可能触发几十上百次。不加控制地执行回调,会频繁读取 scrollTop、scrollHeight 等属性,触发重排重绘,甚至发起重复请求,导致卡顿、掉帧或内存泄漏。
核心原则是:不是等滚动停了再处理,而是按固定节奏“匀速响应”滚动变化。
节流的关键在于控制“判断逻辑”的频率,而不是压制滚动本身
比如懒加载、吸顶导航、虚拟列表可视区域计算,都需要持续感知滚动位置,但不需要每次像素级变化都响应。节流就是给这些判断动作设一个最小执行间隔(如 100ms 或 16ms),既保持响应感,又避免过载。
推荐两种稳定可靠的实现方式
-
时间戳版(轻量、无定时器、不易出错)
记录上一次执行时间,当前时间减去它,超过阈值才执行并更新时间戳:function throttle(func, delay) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { func.apply(this, args); lastTime = now; } }; }适合长期监听的场景,无定时器残留风险,
delay设为16可对齐 60fps 刷新节奏。立即学习“Java免费学习笔记(深入)”;
-
定时器版(支持 trailing 补发,适合需捕捉“滚动结束”的情况)
用setTimeout控制冷却期,配合clearTimeout防重复排队:function throttle(func, delay, options = {}) { const { leading = true, trailing = false } = options; let timer = null; let lastExecTime = 0; return function(...args) { const now = Date.now(); const elapsed = now - lastExecTime; if (leading && elapsed >= delay) { func.apply(this, args); lastExecTime = now; return; } if (trailing) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); lastExecTime = Date.now(); }, delay); } }; }trailing: true适用于懒加载最后一屏这类需要“兜底执行”的场景;多数性能优化推荐leading: true, trailing: false。
实际使用中还要注意三点
-
加上
{ passive: true }
告诉浏览器你不会调用preventDefault(),启用异步滚动,大幅提升滑动流畅度:window.addEventListener('scroll', handler, { passive: true }); -
结合业务状态锁(如
isLoading)
防止加载中再次触发,避免重复请求:let isLoading = false; function handleScroll() { const scrollTop = window.scrollY; const threshold = 150; if (scrollTop + window.innerHeight >= document.body.scrollHeight - threshold && !isLoading) { isLoading = true; loadMore().finally(() => isLoading = false); } } window.addEventListener('scroll', throttle(handleScroll, 160)); -
避免内存泄漏
组件卸载时记得移除监听器,尤其在 React/Vue 等框架中:useEffect(() => { window.addEventListener('scroll', handler, { passive: true }); return () => window.removeEventListener('scroll', handler); }, []);
不复杂但容易忽略。


















