Skip to content
返回设计中心

长列表虚拟化

仅渲染可见切片以窗口化 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 仅是滚动条提示

常见问题解答