为什么需要虚拟列表
在渲染大量数据时,如果直接使用 map 将所有列表项全部生成 DOM 节点,会导致严重的性能问题。例如,一个包含 10 万条数据的表格,一次性渲染会占用大量内存,引发页面卡顿甚至崩溃。真实场景中,用户一次只能看到视口内有限数量(通常几十条)的记录,其余内容完全不需要提前渲染。
虚拟列表(Virtual List)的思路就是:只渲染用户当前可视区域内的列表项,当用户滚动时动态计算并替换可视区域内的元素。这样无论总数据量多大,实际渲染的 DOM 节点数量始终维持在可控范围内,从而保证流畅的滚动体验。
react-window:轻量高性能的虚拟列表库
react-window 是当前 React 生态中使用最广泛的虚拟列表库之一,由 React 核心团队成员 Brian Vaughn 开发。它极其轻量(bundle 仅约 3KB),API 简洁清晰,支持固定高度和可变高度的列表、网格等多种布局。
基本用法:固定行高列表
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>
Row {index}
</div>
);
function VirtualizedList() {
return (
<List
height={400} // 列表可视区域高度
itemCount={100000} // 数据总量
itemSize={35} // 每行高度(px)
width={300}
>
{Row}
</List>
);
}
react-window 会自动计算可视区域内应该显示哪些项,然后调用 Row 组件渲染每一项,并通过 style 属性控制其位置。开发者无需手动计算滚动偏移量。
动态行高的支持
对于行高不固定的列表,可以使用 VariableSizeList,并提供一个 itemSize 函数或估计值:
import { VariableSizeList as List } from 'react-window';
const getItemSize = index => (index % 2 === 0 ? 50 : 80);
function Row({ index, style }) {
return <div style={style}>Row {index}</div>;
}
function DynamicList() {
return (
<List
height={400}
itemCount={1000}
itemSize={getItemSize} // 根据 index 返回行高
width={300}
>
{Row}
</List>
);
}
如果行高依赖实际渲染内容而无法提前预知,可以结合 useRef 和 measureElement 实现自动测量,但需要注意性能开销。
与真实数据结合
通常会从父组件传入数据数组,然后在 Row 中通过 data 属性访问:
const Row = ({ index, style, data }) => (
<div style={style}>{data[index].name}</div>
);
function App() {
const items = useMemo(() => fetchLargeData(), []);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
itemData={items} // 传递给 Row 的额外数据
width={300}
>
{Row}
</FixedSizeList>
);
}
实用场景扩展
- 滚动到指定项:通过
ref调用scrollToItem(index)方法 - 无限滚动加载:监听
onScroll事件,当接近底部时加载更多 - 网格布局:使用
FixedSizeGrid或VariableSizeGrid渲染二维列表
react-virtualized:功能更全面但更重的选择
react-virtualized 是 react-window 的前身,同样由 Brian Vaughn 开发。它提供了更丰富的组件集合,包括 List、Grid、Table、Collection 等,还内置了自动调整大小(AutoSizer)、滚动条定制、无限加载(InfiniteLoader)等功能。
然而,react-virtualized 的 bundle 体积较大(约 30KB gziped),且 API 相对复杂。目前绝大多数新项目都已经转向轻量的 react-window,react-virtualized 主要存在于一些对特定组件(如 Table、CellMeasurer)有强依赖的遗留项目中。
典型用法对比
// react-virtualized 需要更多配置
import { List, AutoSizer } from 'react-virtualized';
function rowRenderer({ index, key, style }) {
return (
<div key={key} style={style}>
Row {index}
</div>
);
}
function VirtualizedList() {
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={1000}
rowHeight={35}
rowRenderer={rowRenderer}
width={width}
/>
)}
</AutoSizer>
);
}
选型建议与注意事项
| 特性 | react-window | react-virtualized |
| ------------------ | --------------------- | ---------------------- |
| Bundle 体积 | ~3KB gziped | ~30KB gziped |
| 学习成本 | 低 | 较高 |
| 社区活跃度 | 活跃,主流新项目首选 | 维护但新增功能较少 |
| 内置高级功能 | 基础 | 丰富(AutoSizer、CellMeasurer等) |
| 动态行高支持 | 需自行测量或使用 VariableSizeList | 内置 CellMeasurer 自动测量 |
推荐做法:
- 新项目直接选择 react-window,它满足了 95% 的虚拟列表需求,且性能极佳。
- 如果需要自动行高测量、复杂表格、自动尺寸检测等高级特性,可考虑 react-virtualized,或引入
react-window+ 自己实现 AutoSizer(仅需少量代码)。 - 避免在虚拟列表项内使用过于复杂的嵌套组件,因为频繁的重建和回收会放大性能问题。
- 结合
React.memo优化Row组件,避免不必要的重新渲染:
const Row = React.memo(({ index, style, data }) => {
return <div style={style}>{data[index]}</div>;
});
- 注意,虚拟列表本质上是牺牲滚动条的自然行为(DOM 节点数量恒定)来换取性能,因此
scroll事件的scrollTop、target等行为与普通列表有所不同,开发时需留意。
虚拟列表是处理大数据量渲染的利器,正确使用可以让页面滚动如丝般顺滑,是 React 性能优化中不可忽视的一环。