最佳实践与坑
最佳实践
1. key 用业务 id,别用 index
列表项会被反复卸载/重建,用 computeItemKey={(index, item) => item.id}。用 index 当 key,滚动后展开/勾选状态会漂移到别的行。
2. 状态别放在 item 组件内部
虚拟列表的项只保留"视口附近"的,滚走就销毁。展开态、勾选态等状态要提升到父级或全局 store,用 Map<itemId, state> 缓存,否则滚回来状态就丢了:
function List() {
const [expanded, setExpanded] = useState(() => new Map());
return (
<Virtuoso
data={items}
computeItemKey={(i, item) => item.id}
itemContent={(index, item) => (
<Row
item={item}
expanded={expanded.get(item.id) ?? false}
onToggle={() =>
setExpanded((prev) => {
const next = new Map(prev);
next.set(item.id, !prev.get(item.id));
return next;
})
}
/>
)}
/>
);
}
3. 给确定的高度
虚拟列表必须知道"视口多高"。style={{ height: '400px' }} 或父容器定高都行;容器高度是 auto / 内容撑开时,虚拟化会失效。
4. 动态内容记得给估算值
图片懒加载、异步渲染会让测量滞后。给 defaultItemHeight 一个接近真实值的估算,能减少首屏和滚动时的位置跳动。
5. 数据量不同,策略不同
| 量级 | 策略 |
|---|---|
| < 1000 | 不一定需要虚拟化,直接渲染 |
| 1000 ~ 5000 | 可用,配合 endReached 无限加载 |
| 5000 ~ 10 万+ | 虚拟列表主场,computeItemKey + 状态提升必须做好 |
常见坑
| 坑 | 现象 | 解法 |
|---|---|---|
| 滚动白屏 / 跳空 | overscan 太小或高度测量偏差 | 调大 overscan;给 defaultItemHeight |
| 图片加载后跳动 | 高度从 0 变真实高度 | 图片容器定宽高 / aspect-ratio,或 loading="lazy" |
| 卸载再挂载状态丢失 | 展开/勾选回不去 | 状态提升 + Map 缓存(见上) |
容器 height: auto | 不虚拟化 / 全部渲染 | 给确定高度 |
| 弹层渲染到 body | 视觉定位异常 | 弹层容器放列表项内,或配合 customScrollParent |
| 快速滚动掉帧 | 项内部太重(大图/复杂计算) | 项组件 memo;图片懒加载 |
和 react-window 怎么选
| react-window | react-virtuoso | |
|---|---|---|
| 包体积 | ~4-7KB | 更大(~30KB+) |
| 动态高度 | 手动维护测量 | 自动 |
| 表格 / 网格 / 分组 / 聊天 | 无 | 内置 |
| 新项目推荐 | 定高 + 极致体积 | 默认推荐(开箱即用) |
一句话:定高、极简、追求最小包 → react-window;动态高度、内容不可控、要表格/瀑布流/分组/聊天 → react-virtuoso。
一句话总结
react-virtuoso 是"动态高度开箱即用"的虚拟列表库:
Virtuoso管通用列表(无限加载、聊天跟随),VirtuosoGrid管瀑布流,TableVirtuoso管海量表格,GroupedVirtuoso管分组吸顶。记住三条铁律——key 用业务 id、状态提到父级、容器给确定高度,大部分坑都能避开;动态内容给个defaultItemHeight估算,性能就稳了。