滚动卡顿主因是scroll中触发强制同步布局,应避免直接操作dom、用transform替代top/margin、节流读取布局、加passive: true、用translateZ(0)或will-change提升图层、长列表必用虚拟滚动。

滚动卡顿是因为触发了重排(reflow)
绝大多数 html5 页面滚动不流畅,根源在于监听 scroll 事件时直接操作 DOM 或读写 offsetTop、getBoundingClientRect() 等会强制浏览器同步计算布局。这种“强制同步布局”在每帧都发生,极易导致掉帧。
- 避免在
scroll回调里调用element.style.top = ...或修改 class 触发位置/尺寸变化 - 用
transform: translateY()替代top/margin-top—— 它走合成层(compositor),不触发重排 - 读取布局信息(如
el.getBoundingClientRect())尽量节流或移到requestIdleCallback中
使用 passive: true 防止 scroll 事件阻塞主线程
移动端浏览器默认把 scroll 事件标记为“可能调用 prEventDefault()”,因此会等待 js 执行完才滚动,造成明显延迟。显式声明 passive: true 可解除该阻塞。
window.addEventListener('scroll', handleScroll, { passive: true // 关键:告诉浏览器你不会调用 preventDefault() });
- 若确实需要阻止滚动(如下拉刷新),改用
touchstart+touchmove并在必要时设passive: false - 不加
passive: true时,chrome 控制台会警告 “Unable to preventDefault inside passive event listener” - 兼容性没问题:Chrome 51+、firefox 53+、safari 11.1+、edge 79+ 均支持
css 层级和 will-change 让滚动走 GPU 合成
即使用了 transform,如果元素没被提升为独立图层,仍可能和父容器共用渲染层,导致滚动时频繁重绘。需主动触发图层分离。
- 对滚动中要动画的元素加
transform: translateZ(0)或will-change: transform - 慎用
will-change:只在滚动开始前设置,滚动结束 100ms 后移除,避免内存浪费 - 避免给整个
或长列表父容器设will-change,它会导致大量纹理内存占用 - 用 chrome devtools → Rendering → “Paint flashing” 和 “Layer borders” 查看是否成功分层
虚拟滚动(virtual scrolling)是长列表唯一靠谱解法
当列表项超 200 行,哪怕所有优化都做了,DOM 节点过多仍会拖慢滚动。此时必须放弃渲染全部节点。
立即学习“前端免费学习笔记(深入)”;
- 核心逻辑:只渲染视口内 + 上下各缓冲 1~2 屏的节点,其余用空白占位(
height精确控制) - 监听
scroll时仅更新scrollTop和当前渲染起始索引,不新增/删除 DOM - 推荐轻量方案:
react-window(React)、vue-virtual-scroller(Vue),或手写基于IntersectionObserver的简易版 - 别用
display: none或visibility: hidden隐藏行——它们仍参与布局计算
滚动性能问题往往不是单一原因,而是“强制同步布局 + 无图层分离 + 全量 DOM 渲染”三者叠加。真正有效的优化,是先用 DevTools 的 Performance 面板录制一次滚动,看火焰图里哪一帧卡在 Layout 或 Paint,再针对性切掉那个环节。