Vue3 虚拟列表实战:海量数据渲染优化
背景与问题界定 在后台管理系统中,数据看板和日志查询面板经常需要渲染数千甚至数万条记录。直接使用 v-for 渲染全部 DOM 节点会导致首次渲染耗时数百毫秒、滚动严重掉帧,Chrome DevTools 的 Performance 面板会清晰呈现主线程长时间被 Layout 和 Paint 占据的场景。虽然各大组件库均提供了虚拟滚动方案,但实际项目中总有定制需求——行高动态变化、表头冻结、分组头部、行内展开详情等,通用组件往往在这些场景下力不从心。我们需要自建一套适配业务场景的虚拟列表方案,覆盖从 1 万到 100 万条数据的渲染需求。 目标拆解与工程约束 DOM 节点数恒定在可视区 3 倍以内:无论数据总量多少,实际渲染的 DOM 节点数必须控制在可视行数的 2 ~ 3 倍,超出部分通过 padding-top/padding-bottom 占位。 支持动态行高:无法预先获知每行高度时,必须提供高度缓存与估算回退机制,避免滚动时出现大幅跳跃。 滚动位置恢复:用户离开列表页再返回时,应恢复滚动位置和展开状态,避免频繁重新请求全部数据。 兼容 keep-alive 和 transition-group:虚拟列表内部不冲突 Vue 的缓存机制,支持列表项的入场动画(如 fadeIn)。 方案设计 虚拟列表的核心思想是只渲染可视区域及其上下缓冲区内的节点。我们实现一个通用的 useVirtualList 组合函数,接收数据源和配置项,返回可视数据切片和容器样式绑定。 基础架构分为三层:容器层负责监听滚动事件、计算可视范围;数据层维护扁平化的行索引与高度映射;渲染层通过 v-for 仅迭代 visibleItems。对于固定高度场景,实现最简单——用行高乘以总行数算出总高度,计算 scrollTop / rowHeight 得到起始索引。动态高度场景则需要一个 高度缓存表:首次渲染时所有行按估算高度占位,渲染完成后通过 ResizeObserver 或 getBoundingClientRect 获取真实高度并更新缓存。 export function useVirtualList<T>(items: Ref<T[]>, options: VirtualOptions) { const { itemHeight: estimatedHeight, buffer = 2 } = options const containerRef = ref<HTMLElement | null>(null) const heights = new Map<number, number>() const totalHeight = computed(() => items.value.reduce((sum, _, i) => sum + (heights.get(i) ?? estimatedHeight), 0) ) const startIndex = computed(() => findStartIndex(containerRef.value!.scrollTop, heights, estimatedHeight)) const visibleCount = computed(() => Math.ceil(containerRef.value!.clientHeight / estimatedHeight) + buffer) const visibleItems = computed(() => items.value.slice(startIndex.value, startIndex.value + visibleCount.value) ) // 滚动时同步更新偏移量 const offsetY = computed(() => sumHeights(heights, startIndex.value, estimatedHeight)) return { containerRef, visibleItems, totalHeight, offsetY, updateHeight } } 对于百万级场景,computed 的 totalHeight 求和可能成为瓶颈,需要使用 shallowRef + 手动触发更新替代全量响应式追踪,同时将高度缓存表升级为 分段树(Segment Tree) 以支持 O(log n) 的索引查找。 ...