虚拟滚动必须用固定行高+position: absolute+startIndex线性计算,禁用动态测高和display/visibility隐藏,节点池需用dataset.id映射复用,phantom层高度须精确为data.length×itemHeight且为容器首子元素。

大屏和长列表场景下,指望浏览器自动回收 DOM 节点是错的——disconnectedCallback 不保证执行,connectedCallback 不重复触发,滚动中反复 removeChild() + appendChild() 会直接拖垮帧率。
为什么 scrollTop 监听后直接 remove/append 就卡死
常见错误是:监听 scroll,一动就 el.remove(),再 createElement() + appendChild()。这导致每帧都触发回流、事件重绑、内存抖动。
- 一个 200 节点的列表每秒滚动更新 10 次 → 等效每秒创建/销毁 2000 个节点
- 低端设备 layout 时间飙升 3–5 倍,
RangeError: Maximum call stack size exceeded频发 -
display: none或visibility: hidden无效:节点还在内存里,样式树、布局树照常维护
固定行高 + position: absolute + startIndex 计算是最稳路径
动态测高在滚动中调用 getBoundingClientRect() 会触发强制同步布局(layout thrashing),比全量渲染还慢。必须用线性映射:startIndex = Math.floor(scrollTop / itemHeight)。
- 行高差异超 ±5px 就开始视觉错位;超 ±15px 用户明显感知“跳行”
- 占位层必须设
position: relative,否则absolute子元素的top会相对错位祖先定位 - 用
top而非transform: translateY():编辑器场景下 selection 坐标系更准,Chrome 115–122 中后者易引发额外合成层
节点池复用必须带 dataset.id + 不 detach
缓存节点不能靠数组索引(易漂移),得用 Map 键值对绑定唯一标识;复用时只改内容、不删不建,避免事件残留和 input 焦点丢失。
立即学习“前端免费学习笔记(深入)”;
- 回收时机:节点滚出视口时,
node.style.display = 'none'并存入nodePool.set(node.dataset.id, node) - 获取逻辑:先查
nodePool.get(id),存在则node.style.display = '',再批量更新textContent、dataset.updated等字段 - 所有节点初始化就必须写死结构:
class、子元素class名、contenteditable属性,禁止复用时增删子节点
撑高滚动条的 phantom 层不能少,且高度必须精确
滚动条失效或错位,90% 是因为 .phantom 高度没算对,或它没在 DOM 正确位置上。
-
height必须为data.length * itemHeight,不能四舍五入,不能用offsetHeight反推 - 它必须是外层容器的**第一个子元素**,且不参与任何事件监听或 contenteditable
- 表格场景要加
table-layout: fixed和显式列宽,否则单元格撑开导致行高波动,破坏线性映射
真正难的不是算 startIndex,而是让每个节点在生命周期内可寻址、可聚焦、可被 getSelection() 正确引用——编辑器里光标一丢,整个虚拟滚动就退化成幻灯片。



















