节流函数通过将高频 scroll 事件压缩为固定间隔执行,避免主线程持续忙于 JS 和重排重绘,从而解决滚动卡顿;配合 passive: true 可进一步消除同步阻塞。

节流函数为什么能解决滚动卡顿
页面滚动时触发 scroll 事件频率极高(尤其在 Chrome 中可达 60–120Hz),如果每次都在监听回调里执行 DOM 查询、样式计算或 getBoundingClientRect(),浏览器主线程会持续忙于 JS 执行和重排重绘,导致掉帧、滚动卡顿甚至输入延迟。节流不是“减少调用”,而是把密集触发压缩成固定间隔的稳定节奏,让浏览器有喘息时间。
手写一个带 leading/trailing 控制的节流函数
很多现成工具库(如 Lodash)的 throttle 默认不支持「首次立即执行 + 结尾补发」组合,而滚动场景常需要:刚滚就立刻响应(比如 sticky 导航显隐),停止后还要兜底一次(比如最后位置触达临界点)。下面这个实现兼顾控制权和轻量:
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=false时,第一次调用会等满wait才执行(适合纯防抖式节流) -
trailing=false时,不会在最后一次触发后补调,适合对“结束态”不敏感的场景(如仅需平滑动画) - 滚动监听中建议用
{ leading: true, trailing: true },避免临界位置漏判 - 别把
this或event直接传进闭包——滚动事件对象是复用的,节流后拿到的可能是过期引用
滚动监听必须配合 passive: true
现代浏览器对未声明 passive: true 的 scroll 监听器会强制同步阻塞滚动,哪怕你的回调里只有一行 console.log。这是比节流更底层的性能开关:
window.addEventListener('scroll', throttle(handleScroll, 16), {
passive: true
});
注意:
-
passive: true意味着你不能在回调里调用event.preventDefault()(比如阻止默认滚动) - 若需阻止滚动(如自定义滚动容器),必须显式设
passive: false,此时节流只能缓解但无法根治卡顿 - 不加
passive选项时,Chrome DevTools 的 “Rendering” 面板会标黄警告 “‘scroll’ handler tookms”
比节流更优的替代方案:IntersectionObserver + requestIdleCallback
如果节流目标只是“判断元素是否进入视口”,硬上 scroll + throttle 是过重方案。现代浏览器提供了零主线程开销的替代:
-
IntersectionObserver:监听元素可见性变化,完全异步,不触发重排,且支持rootMargin精确控制阈值 -
requestIdleCallback:把非紧急计算(如上报、预加载)塞进浏览器空闲时段,避免抢占渲染 - 两者组合可彻底移除
scroll监听器——例如懒加载图片时,用IntersectionObserver触发requestIdleCallback去设置src
真正难处理的是那些必须依赖实时滚动偏移的逻辑(比如视差滚动、滚动进度条、吸顶高度动态计算),这时候节流仍是不可替代的兜底手段。










