局部滚动卡顿根因是浏览器未创建独立合成层,须同时加-webkit-overflow-scrolling: touch和transform: translateZ(0),容器需明确宽高,touchmove监听器必设{passive: true},禁用同步布局读取,长列表必须虚拟滚动。

局部滚动卡顿不是“样式没调好”,而是浏览器没把它当独立滚动图层处理,尤其在中低端安卓 WebView 上,overflow: scroll 默认走 CPU 软件合成,帧率直接掉到 20–30fps。
必须同时加 -webkit-overflow-scrolling: touch 和 transform: translateZ(0)
只加其中一条,滚动依然卡——iOS 需 -webkit-overflow-scrolling: touch 启用惯性滚动,安卓 WebView 则依赖 transform: translateZ(0) 强制创建独立合成层。两者缺一不可。
- 容器必须有明确宽高(不能是
height: auto或靠内容撑开),否则合成层可能被降级 - 避免给父级设
filter、opacity < 1或mask,这些会干扰图层分离 - 用 Chrome DevTools → Rendering → 勾选 “Layer Borders”,滚动时看容器是否有绿色边框,确认是否生效
touchmove 监听器没设 { passive: true } 就会降级为 JS 驱动
哪怕监听器里只写 console.log(),只要绑了 touchmove,iOS 和多数安卓 WebView 就会禁用原生滚动优化,变成逐帧调 JS,一卡一卡。
- 必须显式传
{ passive: true }:element.addEventListener('touchmove', handler, { passive: true }) - 如果真要拦截(如下拉刷新),改用
touchstart+touchmove组合,并只在必要时机动态设passive: false - 控制台报错
"Unable to preventDefault inside passive event listener"就是没加passive: true
滚动中读取 getBoundingClientRect() 或 offsetHeight 会强制同步布局
每次调用这些 API,浏览器都得立刻重算整个页面流,单次耗时轻松超 10ms,直接掉帧。这不是逻辑慢,是浏览器被“卡住”了。
立即学习“前端免费学习笔记(深入)”;
- 把 layout 读取移到
requestIdleCallback()或节流后的回调末尾 - 用
IntersectionObserver替代手动计算元素是否进视口 - 真要实时读,先缓存一次值(比如在
resize后),滚动中复用,直到resize或orientationchange再更新 - 优先用
element.scrollTop而非window.scrollY,少一次跨文档查询
长列表没做虚拟滚动,DOM 数量压垮渲染线程
超过 200 行的局部滚动容器,哪怕 CSS 和事件全优化到位,低端安卓机仍会卡——不是 JS 慢,是 DOM 树遍历和内存占用太高。
- 只渲染视口内 + 上下各 1–2 屏的节点(通常 ≤ 20 条)
- 用一个
div占位撑起总高度:style="height: calc(var(--item-height) * var(--total-count))" - 滚动时仅更新
scrollTop和当前渲染索引,不增删 DOM - 别用
will-change: scroll-position试图“骗”浏览器优化,旧版 Web 不支持,且易引发内存泄漏
最常被忽略的是:硬件加速两行 CSS 必须共存;passive: true 不是可选项;虚拟滚动对 200+ 行是硬性要求,不是“锦上添花”。这些点任何一个漏掉,卡顿就无法根除。



















