虚拟列表是承载海量流式历史对话最可靠的方式,关键在于滚动、追加、定位均稳而不卡:只渲染可视区及缓冲区节点,dom数量压至几十个;固定行高(如72px)+占位撑高(totalheight = data.length × itemheight)为最稳妥起点;追加新消息需同步更新总高度并智能滚动到底部;锚点定位应避免从头计算,优先用scrollintoview或预设scrolltop触发onscroll;动态高度可通过预估+resizeobserver缓存优化,兼顾精度与性能。

虚拟列表是承载海量流式历史对话最可靠的方式,关键不在于“能不能撑住”,而在于“怎么让滚动、追加、定位都稳而不卡”。它通过舍弃全量 DOM,只维护可视区及少量缓冲区的节点,把 DOM 数量压到几十个,彻底避开内存爆炸和重排重绘瓶颈。
固定高度 + 占位撑高是最稳妥的起点
历史对话每条消息结构相对统一(头像+时间+文本/图片),适合采用固定行高策略。这是性能最可控、实现最轻量的方案。
- 设定统一 itemHeight(例如 72px),所有消息按此高度参与计算
- 用一个 div 撑起 totalHeight = data.length × itemHeight,作为滚动容器的总高度
- 上方用 padding-top 或空 div 占位 topSpacer = startIndex × itemHeight
- 下方同理预留 bottomSpacer,保证滚动条比例始终准确
- 滚动时仅更新 visibleData.slice(startIndex, endIndex),不重建整个列表
流式追加需同步更新占位与索引边界
新消息到来不是简单 push 到数组末尾就完事——必须立刻修正滚动容器的总高度,并判断是否需要自动滚动到底部。
- 追加后立即更新 totalHeight,浏览器会自动调整滚动条长度
- 若当前已滚动到底部(scrollTop + containerHeight ≥ scrollHeight - threshold),则 nextTick 中 scrollTo({ top: scrollHeight })
- 避免在追加瞬间触发重排:不要在追加后立刻读取 scrollHeight,可用 requestAnimationFrame 延迟获取
- 对高频追加(如 WebSocket 批量推送),做简单节流或合并渲染批次,防止连续触发 onScroll 计算
锚点定位与跳转要绕过“从头计算”陷阱
用户点击某条消息想快速定位,传统做法是 findIndex 后 scrollTo(0, index × itemHeight),但若 index 是 8 万,直接 scrollTo 可能因初始 scrollTop 为 0 导致首次滚动卡顿。
- 定位前先手动设置容器 scrollTop = targetIndex × itemHeight,再触发一次 onScroll 模拟滚动事件
- 或者更优:用 scrollIntoView({ block: 'start', behavior: 'auto' }) 配合 ref 绑定到目标虚拟项的 wrapper 元素(该元素需在 visibleData 中存在)
- 不在可视区的目标项,先动态扩展 visibleData 范围(临时增大 buffer),渲染后再滚动,避免白屏
动态高度可支持,但需缓存与降级策略
如果消息含富文本、长图、折叠内容,高度不固定,仍可支持,但代价是精度与性能平衡。
- 首次渲染时用预估高度(如 64px)占位,同时用 ResizeObserver 监听真实高度并缓存到 Map
- 后续滚动中优先查缓存,未命中则继续用预估高度,避免回流
- 总高度不再用 itemHeight × length,改为累加缓存高度 + 未缓存项的预估高度
- 对极端情况(如单条消息超 1000px),限制最大渲染高度,或提供“展开/收起”交互代替无限撑高










