直接在<body>上写onscroll必卡,因其每次触发都重新解析字符串、无法保存闭包状态、强制同步执行且不支持passive: true;必须改用addEventListener('scroll', handler, {passive: true})并配合requestAnimationFrame节流和IntersectionObserver替代。

为什么onscroll直接写在<body>上必卡
因为onscroll是内联事件处理器,每次滚动触发时都会重新解析字符串、新建函数作用域,闭包状态(比如节流用的lastTime)根本存不住——等于没节流。更糟的是,它自动绑定在window上,且无法传{ passive: true },浏览器默认认为你可能调用preventDefault(),于是强制同步等待 JS 执行完才滚动,肉眼可见滞后。
必须改用addEventListener并加passive: true
这是底线级操作,不加就别谈优化。原生onscroll属性完全无法控制监听选项,而addEventListener能显式声明行为语义。
-
window.addEventListener('scroll', handleScroll, { passive: true })—— 关键是passive: true,告诉浏览器“我不会阻止滚动”,解除主线程阻塞 - 若真需要拦截(如下拉刷新),只在
touchstart后、判定方向明确时,再对touchmove临时设passive: false,绝不全局滥用 -
handleScroll必须是稳定引用(不能是箭头函数或内联写法),否则removeEventListener失效,导致内存泄漏
滚动逻辑必须进requestAnimationFrame且带标志位
即使加了passive: true,滚动仍每秒触发几十次。直接在回调里读scrollTop或调getBoundingClientRect(),会频繁触发强制同步布局(forced synchronous layout),一帧里干不完,就掉帧。
- 用
let ticking = false+requestAnimationFrame包裹真实逻辑,确保每帧最多执行一次 - 不要在
requestAnimationFrame回调里再读offsetTop或getBoundingClientRect()——这又会触发回流 - 优先用
document.documentElement.scrollTop或window.scrollY(两者性能差异小,但前者在某些安卓 WebView 中更稳) - 如果只是开关 class 或更新变量,可省略标志位;但凡涉及 DOM 查询或计算,必须加
真正该删掉onscroll的场景:用IntersectionObserver替代
如果你的逻辑本质是“某元素是否进入视口”(比如懒加载图片、吸顶导航切换、触发动画),那onscroll就是错的选择。它靠高频轮询模拟,而IntersectionObserver是浏览器原生异步通知,不触发重排、不占主线程、功耗低。
立即学习“前端免费学习笔记(深入)”;
- 兼容性已不是问题:
IntersectionObserver在 Chrome 51+、Firefox 55+、Safari 12.1+、Edge 79+ 全支持 - 监听底部加载?放一个
<div id="loader-ref"></div>在列表末尾,观察它的isIntersecting即可,不用算scrollHeight和scrollTop - 避免用
document.body.scrollHeight - window.innerHeight 这种写法——iOS Safari 和部分安卓 WebView 因渲染延迟/缩放抖动,常误判“已到底”,导致重复请求
最常被忽略的一点:哪怕你把 JS 逻辑全优化了,只要<body>上有overflow: auto或内容撑高触发原生滚动,浏览器就会为整个文档创建合成层,而onscroll属性会让这个过程更不可控。删掉它不是为了“看起来更现代”,而是为了交出控制权,让浏览器按自己的节奏渲染。



















