长列表虚拟化
仅渲染可见切片以窗口化 1 万+ 行,带稳定 key 与动态高度。
组件模式高级
为什么
渲染 1 万 DOM 节点冻结浏览器数秒并耗 100MB+ 内存。虚拟化仅渲染约 20 可见行,使滚动顺滑且内存恒定,与数据量无关。
复合组件模式被 Radix UI、Headless UI 和 shadcn/ui 采用 — 这是 2024 年采用最多的三个无头库。相比扁平组件 API,它们减少 70% 的属性逐层传递。
何时使用
列表超约 200 行或出现滚动卡顿时虚拟化。以下测量开销不值;原生滚动即可。
如何做
1. 选库 — @tanstack/react-virtual(无头,推荐)或 react-window(全配)
2. 测量 — 固定高度简单;动态需 measureRef 或估算尺寸 + resize observer
3. 渲染 — 仅 [scrollTop, scrollTop + viewportHeight] 内项加 overscan(3-5 行)
4. 占位 — 高 div 带总高度保持滚动条准确
5. key — 每项稳定唯一 key(绝不用数组索引)使 React 跨滚动复用 DOM
6. 滚动容器 — 虚拟器附到滚动父级;确保有界高度
7. 无限加载 — 近底时通过 onItemsRendered 追加,显示哨兵+加载器
示例
import { useVirtualizer } from "@tanstack/react-virtual";
function BigList({ items }: { items: Row[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // 每行 px
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((vi) => (
<div key={items[vi.index].id}
style={{ position: "absolute", top: vi.start, height: 48 }}>
{items[vi.index].name}
</div>
))}
</div>
</div>
);
}
常见问题
- ⚠数组索引作 key — 过滤/排序时行重排,React 复用错 DOM
- ⚠滚动父级无界高度 — 虚拟器测 0px,啥也不渲染
- ⚠动态高度无测量 — estimateSize 错,行重叠或留空
最佳实践
- ✓用稳定唯一 key(item.id)— 绝不用数组索引,防重排时 DOM 复用 bug
- ✓给滚动父级有界高度 — height: 100vh 或固定 px,绝不用 auto
- ✓用 resize observer 测动态高度 — estimateSize 仅是滚动条提示