React 大列表优化中,虚拟列表(Virtual List) 是最常见、最有效的优化方案之一。
为什么需要虚拟列表
假设有 10 万条数据:
{
data.map(item => (
<Row key={item.id} {...item} />
))
}
React 会一次性生成:
<div>...</div>
<div>...</div>
...
(100000个节点)
问题:
-
DOM 节点过多
-
首次渲染慢
-
浏览器布局(Layout)和绘制(Paint)耗时
-
滚动卡顿
-
内存占用高
即使用户当前只能看到 20 条数据,浏览器仍然维护 10 万个 DOM。
虚拟列表核心思想
只渲染可视区域内的数据。
例如:
总数据:100000条
当前屏幕:
----------------
第100条
第101条
...
第120条
----------------
实际上只渲染:
100 ~ 120
共 20 个节点。
滚动时:
向下滚动
100~120
↓
110~130
销毁旧节点,复用或创建新节点。
DOM 数量始终维持:
20 ~ 30个
而不是:
100000个
实现原理
假设:
总数 = 100000
每行高度 = 50px
总高度:
100000 * 50
= 5000000px
容器:
<div className="container">
<div className="phantom"></div>
<div className="content"></div>
</div>
结构:
container
│
├── phantom
│ 高度5000000px
│
└── content
真正渲染的数据
1. 创建占位元素(phantom)
<div
style={{
height: data.length * itemHeight
}}
/>
作用:
让滚动条看起来像真的有 10 万条数据。
滚动条长度正常
但实际上没有渲染全部节点。
2. 监听滚动
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop;
};
例如:
scrollTop = 5000px
3. 计算开始索引
公式:
startIndex = Math.floor(
scrollTop / itemHeight
);
例如:
5000 / 50
= 100
说明:
当前从第100条开始显示
4. 计算结束索引
可视区域:
containerHeight = 500px
可显示:
500 / 50
= 10条
结束索引:
endIndex = startIndex + visibleCount;
100 + 10
= 110
5. 截取数据
const visibleData =
data.slice(
startIndex,
endIndex
);
只渲染:
{
visibleData.map(...)
}
6. 偏移内容位置
如果直接渲染:
100~110
会出现在顶部。
需要移动到正确位置:
offsetY =
startIndex * itemHeight;
100 * 50
= 5000px
<div
style={{
transform: `translateY(${offsetY}px)`
}}
>
最终效果:
滚动5000px
↓
显示第100条
图解
总高度 5000000px
┌──────────────────┐
│ │
│ phantom │
│ │
└──────────────────┘
↑
当前滚动到这里
↓
translateY(5000px)
┌──────────────────┐
│ 第100条 │
│ 第101条 │
│ 第102条 │
│ ... │
└──────────────────┘
简易实现
function VirtualList() {
const itemHeight = 50;
const visibleCount = 10;
const [start, setStart] = useState(0);
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop;
setStart(
Math.floor(scrollTop / itemHeight)
);
};
const visibleData = data.slice(
start,
start + visibleCount
);
return (
<div
style={{
height: 500,
overflow: "auto"
}}
onScroll={handleScroll}
>
<div
style={{
height:
data.length * itemHeight,
position: "relative"
}}
>
<div
style={{
transform: `translateY(${
start * itemHeight
}px)`
}}
>
{visibleData.map(item => (
<div
key={item.id}
style={{
height: itemHeight
}}
>
{item.name}
</div>
))}
</div>
</div>
</div>
);
}
优化:缓冲区(Buffer)
实际项目不会只渲染可视区域。
例如:
可视区域:10条
上缓冲:5条
下缓冲:5条
实际渲染:
20条
这样滚动时不会频繁白屏。
const buffer = 5;
start =
Math.max(
0,
startIndex - buffer
);
end =
startIndex +
visibleCount +
buffer;
动态高度怎么办?
上面算法要求:
固定高度
如果每一项高度不同:
50px
80px
120px
60px
...
就不能简单用:
scrollTop / itemHeight
需要维护:
高度缓存表
例如:
[
50,
130,
250,
370,
...
]
通过二分查找定位:
scrollTop
↓
对应哪一项
这就是很多虚拟列表库复杂的地方。
React常用虚拟列表库
1. react-window 官网
作者:Brian Vaughn
特点:
-
轻量
-
性能好
-
推荐
npm install react-window
2. react-virtualized 官网
特点:
-
功能最全
-
支持表格
-
支持动态高度
缺点:
- 包较大
3. TanStack Virtual 官网
特点:
-
React/Vue/Solid 通用
-
现代项目使用较多
面试回答模板
当面试官问:
React 大列表如何优化?
可以这样回答:
大列表的性能瓶颈主要在于 DOM 数量过多导致的渲染、重排和内存开销。常见方案是使用虚拟列表。虚拟列表的核心思想是只渲染当前可视区域的数据,而不是全部数据。通过监听 scroll 事件,根据 scrollTop 计算 startIndex 和 endIndex,仅渲染这一段数据,同时利用一个等高占位元素撑开滚动高度,并通过 translateY 将当前渲染内容移动到正确位置。这样无论数据量是 1 万条还是 10 万条,页面中实际 DOM 数量都保持在几十个,从而大幅提升性能。