虚拟列表解决的问题
在实际后台系统中,我们经常遇到这样的场景:用户列表有 10 万条数据,订单列表 20 万条,日志甚至达到百万级。表格里往往还包含多列、复杂 DOM 结构、hover 交互、操作按钮和状态标签。
如果直接通过 map 渲染所有数据:
data.map(item => <Row key={item.id} />)
你会面临首次渲染卡死、滚动严重掉帧、内存暴涨,甚至浏览器直接崩溃的风险。
核心瓶颈其实很简单:DOM 节点太多。浏览器并不怕 JavaScript 的计算量,最怕的是成千上万个 DOM 节点同时存在。
虚拟列表的本质就是只渲染可视区域内的列表项,其余部分用占位高度'假装存在',从而保持 DOM 数量恒定。
核心设计思路
理解虚拟列表,关键在于把握以下四点:
- 可视区域(viewport):屏幕当前能看到的实际高度。
- 列表总高度:假设所有 item 都渲染后的总高度(虽然不真实渲染,但必须计算出来以撑开滚动条)。
- 起始索引和结束索引:根据当前的滚动距离,计算出应该显示哪几条数据。
- 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置。
实现原理详解
假设每一项高度固定,这是最简单的情况,实际项目中大量列表数据通常也是高度固定的。
例如:itemHeight = 50px,容器高度 500px。
那屏幕最多能显示:500 / 50 = 10 条。
通常会多渲染几条作为缓冲区(buffer),防止滚动过快出现白屏:实际渲染 = 10 + 4 = 14 条。
计算逻辑
根据滚动距离算索引:
startIndex = Math.floor(scrollTop / itemHeight)
endIndex = startIndex + visibleCount
不渲染所有 DOM,但要让滚动条是对的。我们需要一个占位元素撑开高度:
<div>
<div></div> <!-- 撑开高度 -->
<div></div> <!-- 只放可见项 -->
</div>
CSS 样式上:
.phantom { : totalCount * itemHeight; }

