scroll事件监听器会阻塞滚动流畅性,因浏览器需等待监听器执行完才合成帧;passive: true可解除阻塞,但禁止调用preventdefault(),且需特性检测兼容性。

为什么 scroll 事件监听器会阻塞滚动流畅性
浏览器在触发 scroll 事件前,必须等所有注册的监听器执行完,才能继续合成帧——哪怕你只读取 event.target.scrollTop。如果监听器里有同步计算、DOM 查询或意外的重排(如读取 offsetHeight),就会让主线程卡住,导致掉帧、卡顿甚至“滚动延迟”。
尤其在移动端,系统级滚动默认是异步且非阻塞的,但 JS 监听器会强制它进入“可取消”模式(即等待 JS 判断是否要 preventDefault()),这直接拖慢触摸响应。
passive: true 的真实作用和启用条件
passive: true 告诉浏览器:“这个监听器绝不会调用 event.preventDefault()”,于是浏览器可以放心地不等待它执行,直接继续滚动合成。这是性能提升的关键前提。
但注意:一旦你写了 event.preventDefault()(比如阻止默认滚动、实现自定义下拉刷新),就必须设为 passive: false(默认值),否则会抛出错误:
Unable to preventDefault inside passive event listener due to target being treated as passive
- 仅对
touchstart、touchmove、wheel、mousewheel、scroll等可取消的滚动手势类事件生效 - Chrome 56+、Firefox 59+、Safari 15.4+ 支持;iOS Safari 15.4 之前不支持
scroll的passive,但支持touchmove - 不能和
preventDefault()共存——不是“建议不用”,而是“用了就报错”
如何安全地添加 passive 的 scroll 监听器
别直接写 addEventListener('scroll', handler, { passive: true }) 就完事。要考虑兼容性和降级:
- 用特性检测代替 UA 判断:
'passive' in addEventListener是最可靠方式 - 封装一个带 fallback 的注册函数,避免重复写逻辑
- 如果监听器里有潜在的
preventDefault()(比如未来可能加下拉刷新),现在就别设passive: true
简单示例:
const options = 'passive' in addEventListener ? { passive: true } : false;
window.addEventListener('scroll', onScroll, options);
注意:false 在第三个参数位置等价于 {}(即非 passive),不是语法错误。
passive 不解决所有滚动性能问题
设了 passive: true 只是移除了“等待 JS 执行”的阻塞,并不减少 JS 自身开销。如果你的 onScroll 里频繁操作 DOM、触发重排或没做节流,照样卡。
- 仍需配合
requestAnimationFrame或节流(如 Lodash 的throttle(..., 16))控制执行频率 - 避免在监听器中读写布局属性(
offsetTop、getBoundingClientRect()等),改用IntersectionObserver或ResizeObserver替代 -
passive对scroll的优化效果,在桌面端不如移动端明显——因为桌面滚动本身不依赖触摸事件流
真正卡顿的根源,往往藏在你以为“只是读个 scrollTop”的那一行代码里。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











