虚拟滚动必须手动池化、固定行高、禁用动态测高,否则导致卡顿、内存泄漏、光标错位;需线性映射 startIndex、严格控制 phantom 高度、节点结构固定且 ID 唯一、复用不 detach、禁止同步布局触发。

虚拟滚动不是“开了就省内存”,而是必须手动池化、固定行高、禁用动态测高,否则滚动卡顿、内存泄漏、光标错位会立刻暴露。
为什么 scrollTop + getBoundingClientRect() 一用就卡
滚动中调用 getBoundingClientRect() 会强制触发同步布局(layout thrashing),比全量渲染还慢。浏览器每帧都要重算整个布局树,200 行列表每秒滚动 10 次 → 等效每秒强制 layout 2000 次。
- 正确路径是线性映射:
startIndex = Math.floor(scrollTop / itemHeight),前提是行高严格固定 - 行高偏差超 ±5px 就视觉跳行;超 ±15px 用户明显感知“抖动”
- 表格场景必须加
table-layout: fixed和显式列宽,否则单元格撑开破坏高度一致性
节点池复用必须用 Map + dataset.id,不能靠数组索引
数组索引在滚动中极易漂移:用户快速来回滚动、插入/删除数据、服务端分页重载后,nodes[2] 可能对应完全不同的数据项,导致内容错位、事件绑错、input 焦点丢失。
- 每个节点初始化时必须写死
dataset.id(如item-12345),值来自后端唯一标识,禁止用index或临时 UUID - 缓存必须用
Map:nodePool.set(el.dataset.id, el),回收时只设el.style.display = 'none',不 detach - 复用时先
nodePool.get(id),存在则el.style.display = '',再批量更新textContent、dataset.updated、src等可变字段 - 禁止在复用时调用
innerHTML = ''、appendChild()或removeChild()—— 结构必须初始化即固定
phantom 层高度不准,90% 的滚动条失效都源于此
.phantom 是撑高滚动容器的占位元素,它不渲染内容,只负责告诉浏览器“这个列表本该有多长”。高度错 1px,滚动条缩放、拖拽位置、scrollTop 映射全部失准。
立即学习“前端免费学习笔记(深入)”;
- 高度必须精确为
data.length * itemHeight,不能四舍五入,不能用offsetHeight反推 - 它必须是滚动容器的**第一个子元素**,且父容器需设
position: relative - 若用
transform: translateY()定位真实内容区,在 Chrome 115–122 中易触发额外合成层,GPU 内存暴涨;优先用top - 所有节点初始结构必须写死:
class、子元素class、contenteditable属性,禁止复用时增删 DOM
真正难的不是算 startIndex,而是让每个节点在生命周期内可擦写、可聚焦、可安全更新——结构固定、ID 唯一、池不 detach、phantom 不偏移,缺一不可。



















