在虚拟滚动技术中,最容易踩的坑往往源于“想当然”的假设。一个核心观点是:采用固定行高是确保虚拟滚动稳定部署到生产环境的基础。不要被那些所谓的“自动测高”或“动态高度支持”所迷惑——只要行高不固定,`Math.floor(scrollTop / itemHeight)` 这个计算就会失效,导致索引错乱和内容跳变,最终出现白屏。在真实业务场景中,行高差异超过±5px就足以让用户感受到“跳行”,而频繁调用 `getBoundingClientRect()` 获取每行真实高度会触发强制同步布局(layout thrashing),其性能开销甚至超过全量渲染。

因此,方案必须预先固定:所有列表项的高度必须严格一致,例如统一设置为 `height: 48px` 或 `line-height: 48px`,同时禁用 `flex` 自适应、`min-height`、`padding` 波动等可能导致高度变化的干扰属性。在表格场景中,需要添加 `table-layout: fixed` 并显式定义列宽,否则单元格内容撑开会导致高度失控。
接下来讨论定位问题——这是虚拟滚动中翻车率最高的环节。如果将 `position: absolute` 的内容区直接放在滚动容器内,而不为外层容器设置 `position: relative`,那么使用 `top` 或 `transform: translateY()` 偏移内容时,偏移量会相对于最近的定位祖先(如 `body` 或某个父级 modal),导致整个列表飘出视口、滚动条失效,甚至出现空白。
另外,虽然 `transform: translateY()` 在视觉上也能实现类似效果,但在 Chrome 115 到 122 版本中,它会意外触发额外的合成层,导致 GPU 内存消耗激增。相比之下,`top` 属性更可控,且没有这种副作用。每次更新前必须清空旧节点:使用 `contentEl.replaceChildren()`(现代浏览器)或 `contentEl.innerHTML = ''`(兼容 IE11),否则残留的事件监听器、Vue/React 中 `key` 的冲突、input 焦点丢失等问题会迅速出现。
滚动监听必须进行节流。`scroll` 事件在 Chrome 和 Firefox 中每秒可触发 60 到 120 次,不加节流意味着每秒执行大量 DOM 操作。虽然 `lodash.throttle` 可行,但更推荐使用 `requestAnimationFrame`——它天然与屏幕刷新率对齐,并保证同一帧内只执行一次计算和渲染。
示例逻辑大致如下:
```javascript let isPending = false; function handleScroll() { if (!isPending) { isPending = true; requestAnimationFrame(() => { const scrollTop = container.scrollTop; const startIndex = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER_SIZE; const endIndex = Math.min(data.length, startIndex + VISIBLE_COUNT + BUFFER_SIZE * 2); renderRange(startIndex, endIndex); isPending = false; }); } } ```还有一个容易被忽略的点:缩放页面(如 Ctrl+/-)后,`clientHeight` 和 `itemHeight` 的实际像素值已经变化,必须重新计算 `VISIBLE_COUNT` 和缓冲区参数,否则可视范围会错位。
最后来说撑高容器的“phantom”元素。很多人误以为使用 `display: none` 或 `visibility: hidden` 就能解决问题——实际上,它们只是隐藏视觉,并未从 DOM 中移除。1 万条数据就意味着 1 万个 `div` 持久驻留在内存中,浏览器仍需维护样式树、布局树、图层合成信息,重排重绘的开销丝毫未减。虚拟滚动的核心思想是“不创建”,而非“藏起来”。
正确的做法是使用一个空 `div` 来占满总高度:`style="height: ${ITEM_HEIGHT * data.length}px"`。这个元素仅负责撑开滚动条长度,内部不包含任何子节点。它必须是滚动容器的直接子元素,且不能设置 `overflow` 属性,否则会截断内容区。
有一个容易忘记的细节:这个 `phantom` 元素的高度必须实时响应数据长度的变化。当列表支持增删时,每次变更后都要同步更新其 `style.height`,否则滚动条长度会失准,导致拖拽错位,到底部时甚至无法触发加载——这种错位对用户体验的影响非常致命。
