
React性能优化项目复盘虚拟列表在万级数据表格中的实践与坑一、万级数据的渲染灾难一个数据管理后台的日志列表页面后端返回8000条记录。用Ant Design的Table直接渲染页面加载时间约4秒滚动帧率约15fps。问题本质8000个DOM节点同时存在于页面上每个节点都需要布局计算、样式计算和内存占用。用户实际能看到的最多只有20行——剩余7980行白占计算资源。虚拟列表Virtual List的原理只渲染可视区域内的元素滚动时动态替换。二、虚拟列表的实现原理核心计算interface VirtualListState { totalCount: number; // 总行数 itemHeight: number; // 每行高度固定高度 containerHeight: number; // 容器可见高度 scrollTop: number; // 当前滚动位置 } function computeVisibleRange(state: VirtualListState) { const { totalCount, itemHeight, containerHeight, scrollTop } state; // 缓冲区——上下多渲染5行 const overscan 5; // 起始索引 滚动位置 / 每行高度 const startIndex Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); // 可见行数 容器高度 / 每行高度 缓冲区 const visibleCount Math.ceil(containerHeight / itemHeight) overscan * 2; const endIndex Math.min(totalCount, startIndex visibleCount); return { startIndex, endIndex, // 顶部占位高度——让滚动条感觉有8000行 offsetY: startIndex * itemHeight, }; }React实现import { useState, useRef, useCallback, useEffect } from react; function VirtualList({ items, itemHeight, containerHeight }: Props) { const [scrollTop, setScrollTop] useState(0); const containerRef useRefHTMLDivElement(null); const { startIndex, endIndex, offsetY } computeVisibleRange({ totalCount: items.length, itemHeight, containerHeight: containerRef.current?.clientHeight || containerHeight, scrollTop, }); const handleScroll useCallback((e: React.UIEventHTMLDivElement) { setScrollTop(e.currentTarget.scrollTop); }, []); const visibleItems items.slice(startIndex, endIndex); // 总高度 所有行的总高度 const totalHeight items.length * itemHeight; return ( div ref{containerRef} style{{ height: containerHeight, overflow: auto }} onScroll{handleScroll} div style{{ height: totalHeight, position: relative }} {/* 可见区域——绝对定位 */} div style{{ transform: translateY(${offsetY}px) }} {visibleItems.map((item, index) ( div key{startIndex index} style{{ height: itemHeight }} rolerow {renderItem(item)} /div ))} /div /div /div ); }三、实际踩过的坑坑1动态高度。固定高度假设对表格是好的每行30px但对富文本、换行内容不适用。解决方案使用ResizeObserver动态测量每行高度维护一个高度缓存const [rowHeights, setRowHeights] useStateRecordnumber, number({}); // 渲染后测量实际高度 const measureRef useCallback((node: HTMLDivElement | null, index: number) { if (node) { const height node.getBoundingClientRect().height; setRowHeights(prev ({ ...prev, [index]: height })); } }, []);坑2滚动时闪烁。快速滚动时新的可见行还没有渲染完旧的已经卸载——用户看到空白。解决方案增加overscan缓冲区——从5行增加到15行给了浏览器更多渲染时间。坑3行选择状态丢失。用户勾选了第5行的checkbox滚动后第5行被卸载。再滚回来时第5行重新渲染勾选状态消失。解决方案选择状态存储在虚拟列表之外父组件state渲染时从state读取。坑4搜索高亮与虚拟列表的配合。用户搜索error关键词匹配到的行需要高亮显示——但可能不在当前可见区域内。解决方案匹配到error后自动滚动到第一条匹配行并在侧边添加匹配位置标记。四、与Ant Design Table虚拟化的对比Ant Design 5.x内置了虚拟列表支持virtual属性。但实际使用中需要满足以下条件列宽固定不支持width: auto行高固定不支持动态行高不支持合并单元格rowSpan/colSpan当前自研的虚拟列表满足所有需求。代码量约200行实现了固定行高动态行高的混合模式。对比自研 vs 使用库维度自研tanstack/virtual代码量200行引入30KB包灵活性完全控制API约束维护成本自己维护社区维护动态行高支持支持支持最终保留了自研方案——虚拟列表的核心代码就200行没有复杂到需要专门引入一个库。五、总结虚拟列表在万级数据表格中的核心经验核心原理是只渲染可视区域通过总高度占位div让滚动条正常工作缓冲区overscan是防闪烁的关键——上下各渲染5-15行选择状态、搜索高亮等交互状态要存储在虚拟列表外部动态行高用ResizeObserver测量高度缓存——计算复杂度增加约30%性能收益DOM节点从8000降到30加载时间从4秒降到80ms如果数据量在1000行以内普通渲染就够了——虚拟列表的额外复杂度不值得。当数据量超过2000行时虚拟列表是必需品而非优化项。最大的教训不要在所有表格上都加虚拟列表——只在真正需要的地方用。过早优化和过度优化一样有害。