← 中文资源

简体中文版本

虚拟化万行列表且不破坏搜索与滚动

只渲染可见窗口加 overscan,导航返回时恢复滚动位置,并让前缀搜索仍作用于完整数据集。

阅读英文原文

目标

一万行 DOM 意味着八秒的解析布局和每帧为整棵树买单。虚拟化只渲染可见窗口加少量 overscan:DOM 恒定、滚动如原生、数据集再涨 UI 也无感。同时要守住两条底线:搜索过滤作用于完整数组而非渲染切片,滚动位置持久化用稳定 id 而非像素偏移。

步骤要点

  1. 先量基线:统计全文档 DOM 节点数(六位数)并录制滚动 Performance,确认长帧是脚本块所致。
  2. 用 @tanstack/react-virtual 建窗口:估算行高 44、overscan 8,绝对定位加 translateY 摆行,DOM 只剩视口加 16 行。
  3. 过滤在虚拟化之前做:useMemo 对全量数组过滤,输入查询时 scrollToIndex 跳到第一条命中,输入框响应不受万行重渲染拖累。
  4. 滚动恢复走 id:节流把首行 id 写进 URL,恢复时经 idToIndex 换算 scrollToIndex,行高变化后仍落在原记录。
  5. 复测三项:节点数恒定约六百、滚动五秒无长帧、搜索按键响应低于一百毫秒。

排错提示

变高行重叠跳动是 estimateSize 猜错了,给行挂 measureElement 让虚拟器实测高度并在内容变化时重新测量;浏览器 Ctrl+F 找不到未渲染行是窗口化的固有代价,引导用户用自带搜索框并留醒目快捷键提示,全文本工具走导出视图;键盘翻页跳行就把方向与翻页键绑定到 align:auto 的 scrollToIndex,让焦点、选中与视口保持一致。

适用边界

窗口化交出了浏览器原生查找与全 DOM 语义,需要完整树的需求(打印、部分无障碍工具)要有整渲染的回退视图;内存过滤撑到几万行,再往上该换成服务端搜索加分页。

代码示例

before: 10,000 rows mounted
  DOM nodes: ~120,000
  first paint: 8.2s
  scroll: 3-5 long frames/s, janky
  search keystroke latency: 900ms

after: windowed (viewport 80vh / 44px + overscan 8)
  DOM nodes: ~600 (constant, dataset-size independent)
  first paint: 380ms
  scroll: 60fps, no long frames
  search: filters full array in useMemo, jumps to first hit