目标
一万行 DOM 意味着八秒的解析布局和每帧为整棵树买单。虚拟化只渲染可见窗口加少量 overscan:DOM 恒定、滚动如原生、数据集再涨 UI 也无感。同时要守住两条底线:搜索过滤作用于完整数组而非渲染切片,滚动位置持久化用稳定 id 而非像素偏移。
步骤要点
- 先量基线:统计全文档 DOM 节点数(六位数)并录制滚动 Performance,确认长帧是脚本块所致。
- 用 @tanstack/react-virtual 建窗口:估算行高 44、overscan 8,绝对定位加 translateY 摆行,DOM 只剩视口加 16 行。
- 过滤在虚拟化之前做:useMemo 对全量数组过滤,输入查询时 scrollToIndex 跳到第一条命中,输入框响应不受万行重渲染拖累。
- 滚动恢复走 id:节流把首行 id 写进 URL,恢复时经 idToIndex 换算 scrollToIndex,行高变化后仍落在原记录。
- 复测三项:节点数恒定约六百、滚动五秒无长帧、搜索按键响应低于一百毫秒。
排错提示
变高行重叠跳动是 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