rAF 丢最后一帧这个问题确实是硬伤,你给出的修法是对的;不过对于 transform 定位,还得再补两点。
先给 rAF 补充两点:回调里现读 el.scrollTop 是对的,但记得在 useEffect 的 cleanup 里调用 cancelAnimationFrame,否则组件卸载后仍会有一次回调挂在帧队列里。如果确实要追求极致,可以再进一步:在 rAF 回调里直接操作内层的 style.transform,完全不经过 setState,可以省掉一次 React 渲染提交,代价是虚拟列表的状态与 React 脱离,这是适合在几十万行数据时再使用的做法。
transform 有两个实际坑:① 它会创建一个新的层叠上下文,列表项中如果有 position: fixed 的下拉菜单或者 Tooltip,它们会被带上偏移,导致弹层定位错误;这些浮层最好通过 portal 把它挂到 body 上。② ITEM_HEIGHT 必须和真实渲染高度严格相等,建议在内层项上设置 height: 48px; box-sizing: border-box; overflow: hidden,否则每行差 0.5px,滚到几万像素之后就会累积成明显错位。
关于最大高度,还有一点需要提醒:Firefox 的上限比 Chrome 低不少,约 1700 万像素,因此按 Chrome 的 3355 万像素来估算的话,会先在 Firefox 上出错。建议把 1600 万像素作为统一阈值,在 48px 行高下约 33 万条就需要做滚动重映射。
「什么时候不自己写」这部分我完全同意,再补充一个选项:@tanstack/react-virtual 是 headless 的,可以自行决定渲染结构,同时又能直接支持动态行高测量;如果既不想被 react-virtuoso 的封装限制,又想省去编写 ResizeObserver 那部分代码,可以优先考虑它。另外,content-visibility 和虚拟列表叠加基本没有额外收益,两者选一个即可。
最后补充一个你提到的 aria 问题的延伸:仅在数据上加 aria-setsize/aria-posinset 还不够,键盘 Tab 到当前未渲染的项时焦点会丢失或乱跳,通常需要把容器设为 role="list",并使用 aria-activedescendant 方案。

正式会员
认证极客









