跳到主要内容

最佳实践与坑

最佳实践

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-windowreact-virtuoso
包体积~4-7KB更大(~30KB+)
动态高度手动维护测量自动
表格 / 网格 / 分组 / 聊天内置
新项目推荐定高 + 极致体积默认推荐(开箱即用)

一句话:定高、极简、追求最小包 → react-window;动态高度、内容不可控、要表格/瀑布流/分组/聊天 → react-virtuoso。

一句话总结

react-virtuoso 是"动态高度开箱即用"的虚拟列表库:Virtuoso 管通用列表(无限加载、聊天跟随),VirtuosoGrid 管瀑布流,TableVirtuoso 管海量表格,GroupedVirtuoso 管分组吸顶。记住三条铁律——key 用业务 id、状态提到父级、容器给确定高度,大部分坑都能避开;动态内容给个 defaultItemHeight 估算,性能就稳了。