其他
在 TS/JS 中渲染大型列表/网格/表格时,用…
当需要渲染大量数据但只显示视口内项目以保持 60FPS 时,使用 TanStack Virtual 进行虚拟化。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 TS/JS 中渲染大型列表/网格/表通用
解决思路
- 安装:运行 `npm install @tanstack/react-virtual`。
- 使用 `useVirtualizer` 创建虚拟化器,传入必要选项:`count`(项目总数)、`getScrollElement`(返回滚动容器的函数)、`estimateSize`(估算项目大小的函数,建议高估)。
- 可选配置 `overscan`(默认 1)在视口外额外渲染项目数。
- 可选配置 `horizontal`(默认 false)启用水平虚拟化。
- 可选配置 `gap`(默认 0)设置项目间距(px)。
- 可选配置 `lanes`(默认 1)设置车道数(瀑布流/网格)。
- 可选配置 `paddingStart`(默认 0)设置第一个项目前的内边距。
- 可选配置 `paddingEnd`(默认 0)设置最后一个项目后的内边距。
- 可选配置 `scrollPaddingStart`(默认 0)设置 scrollTo 定位偏移。
- 可选配置 `scrollPaddingEnd`(默认 0)设置 scrollTo 定位偏移。
- 可选配置 `initialOffset`(默认 0)设置初始滚动位置。
- 可选配置 `initialRect` 设置初始尺寸(用于 SSR)。
- 可选配置 `enabled`(默认 true)启用/禁用虚拟化。
- 可选配置 `getItemKey`(默认 `(i) => i`)为项目提供稳定 key。
- 可选配置 `rangeExtractor`(默认 default)自定义可见索引。
- 可选配置 `scrollToFn`(默认 default)自定义滚动行为。
- 可选配置 `measureElement`(默认 default)自定义测量。
- 可选配置 `onChange` 状态变化回调。
- 可选配置 `isScrollingResetDelay`(默认 150)设置滚动结束延迟。
- 使用 `virtualizer.getVirtualItems()` 获取可见项目列表。
- 使用 `virtualizer.getTotalSize()` 获取总可滚动大小。
- 使用 `virtualizer.scrollToIndex(index, { align: 'start' | 'center' | 'end' | 'auto', behavior: 'auto' | 'smooth' })` 滚动到指定索引。
- 使用 `virtualizer.scrollToOffset(offset, options)` 滚动到指定偏移。
- 使用 `virtualizer.measure()` 强制重新计算。
- 使用 VirtualItem 的属性:`key`(唯一键)、`index`(源数据索引)、`start`(像素偏移,用于 transform)、`end`(结束像素偏移)、`size`(项目尺寸)、`lane`(车道索引)。
- 核心渲染模式:将滚动容器 ref 赋给 `parentRef`,容器设置固定高度和 `overflow: auto`;内部容器设置 `height: virtualizer.getTotalSize()px`、`width: '100%'`、`position: 'relative'`;遍历 `virtualizer.getVirtualItems()`,每项绝对定位,使用 `transform: translateY(virtualItem.start)` 定位,高度为 `virtualItem.size`。
- 对于动态/可变高度:使用 `measureElement` ref 附加到项目上,并设置 `data-index={virtualItem.index}`(必需),不要设置固定高度,让内容决定高度,`estimateSize` 应高估。
- 水平虚拟化:设置 `horizontal: true`,使用 width 作为容器宽度,使用 `translateX(item.start)` 定位。
- 网格虚拟化:创建两个 virtualizer,一个用于行,一个用于列(列设置 `horizontal: true`),分别获取虚拟项目,渲染单元格时使用 `translateX(column.start) translateY(row.start)` 定位。
- 窗口滚动:使用 `useWindowVirtualizer`,传入 `count`、`estimateSize`、`overscan`、`scrollMargin`(使用 `listRef.current?.offsetTop ?? 0`),渲染时 `translateY(item.start - virtualizer.options.scrollMargin)`。
- 无限滚动:结合 `useInfiniteQuery`,将 `count` 设为 `hasNextPage ? allItems.length + 1 : allItems.length`,在 `useEffect` 中检查最后一个虚拟项目索引,若 `lastItem.index >= allItems.length - 1 && hasNextPage && !isFetchingNextPage` 则调用 `fetchNextPage()`,渲染加载行。
- 粘性项目:使用 `rangeExtractor` 自定义,结合 `defaultRangeExtractor` 和 `stickyIndexes`,返回排序后的索引集合,渲染时设置 `position: sticky; top: 0; zIndex: 1`。
- 平滑滚动:自定义 `scrollToFn`,当 `behavior === 'smooth'` 时使用 `instance.scrollElement?.scrollTo({ top: offset, behavior: 'smooth' })`,否则直接滚动。
- 高估 `estimateSize` 以防止滚动跳跃。
- 增加 `overscan`(3-5)以减少快速滚动时的空白闪烁。
- 使用 `transform: translateY()` 而非 `top` 进行 GPU 合成定位。
- 使用 `measureElement` 进行动态尺寸时,添加 `data-index` 属性。
- 动态测量的项目不要设置固定高度。
- 当项目可能重新排序时,使用 `getItemKey` 提供稳定 key。
- 使用 `gap` 选项代替 margins(margins 会干扰测量)。
- 使用 `paddingStart/End` 代替容器上的 CSS padding。
- 列表隐藏时使用 `enabled: false` 暂停。
- 记忆化回调(`estimateSize`、`getItemKey`、`rangeExtractor`)。
- 在项目上使用 `will-change: transform` CSS 以启用 GPU 加速。
- 避免在动态测量的项目上设置固定高度。
- 避免使用 CSS margins 代替 `gap` 选项。
- 避免忘记在使用 `measureElement` 时添加 `data-index`。
- 避免不在内部容器上提供 `position: relative`。
- 避免低估 `estimateSize`(会导致滚动跳跃)。
- 避免设置 `overscan` 太低(快速滚动时出现空白项)。
- 避免在窗口滚动时忘记从 `translateY` 中减去 `scrollMargin`。
- 避免不记忆化 `estimateSize` 函数(会导致重新渲染)。
适用边界 · 注意事项
- 列表项数量很少(例如少于 100)时,虚拟化带来的复杂度可能不值得。
- 需要所有项目始终存在于 DOM 中以供搜索引擎抓取或打印时。
- 滚动容器没有固定高度或无法确定滚动元素时。