虚拟列表是海量数据渲染的刚性需求,核心是只渲染可视区域;固定高度方案最实用,需用transform定位、节流滚动监听、复用dom节点并注意容器高度与图片高度等细节。

直接渲染几万条数据,页面卡死是必然的,不是代码写得不够好,而是浏览器根本扛不住那么多 DOM 节点。虚拟列表不是“锦上添花”,而是海量数据场景下的刚性需求——它不靠分页、不靠懒加载,只靠「只渲染可视区域」这一条铁律解决问题。
为什么不能用 v-for 或 map() 直接遍历 10 万条数据
常见错误现象:页面白屏、滚动掉帧、内存飙升、DevTools 里 DOM 节点数破万、Performance 面板显示大量 Layout 和 Paint 事件。
- 每个
<li>都会触发样式计算、布局、绘制,数量一多,主线程直接忙不过来 - 即使元素内容为空,仅存在 DOM 树中就已消耗可观内存
-
v-for在 Vue 中默认不复用节点,每次更新都可能重建整个子树 - React 的
map()同理,key 写错或数据结构不稳定时,还会引发无谓重渲染
transform: translateY() 比 top 或 paddingTop 更可靠
定位可视区域内容时,必须让 DOM 元素“出现在正确位置”,但又不能触发重排(reflow)。用 top 或 paddingTop 会强制浏览器重新计算布局,而 transform 属于合成层操作,由 GPU 加速,性能差距明显。
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
-
transform: translateY(${startIndex * itemHeight}px)是最常用且稳定的方式 - 容器需设置
style.overflow = 'auto',内部用一个撑高<div> 模拟总高度:<code>height: ${totalItems * itemHeight}px - 不要用
position: absolute+top,它在快速滚动时容易出现视觉跳动或白屏 - 若用
paddingTop撑开,需同时控制paddingBottom,逻辑更复杂,且 padding 变化仍可能触发 layout - 算出可视行数:
visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2(+2 是缓冲区,防快速滚动白屏) - 滚动时只更新
startIndex = Math.floor(scrollTop / itemHeight),避免频繁计算 - DOM 复用强烈建议:不要每次
innerHTML = ''再拼接,改用documentFragment或预先创建固定数量的<li>节点池 - 滚动监听必须节流:
requestAnimationFrame或 16ms 间隔,否则handleScroll可能在 1 秒内触发上百次 - 注意
scrollTop可能为小数,Math.floor比parseInt更安全;结束索引要Math.min(endIndex, totalItems - 1)防越界 - 容器高度必须是固定值(如
300px),不能是min-height或fit-content,否则clientHeight计算失准 - 撑高用的
<div class="phantom"> 必须放在真实内容之前,否则滚动条初始位置异常<li>如果列表项含图片,记得给 <code><img>设height,否则加载完成时高度突变会破坏 offset 计算 - 移动端 touchmove 事件默认不触发 scroll,要用
{ passive: false }显式注册,否则 iOS 上滚动无响应 - 不要在
scroll回调里直接调用setState(React)或ref.value = ...(Vue),优先用requestIdleCallback或微任务延后更新
固定高度虚拟列表的最小可行实现要点
动态高度虽更真实,但首次渲染慢、内存占用高、索引定位需二分查找——90% 的业务场景用固定高度就够了,先跑通再升级。
哪些地方最容易被忽略
不是实现不出来,而是上线后才发现滚动卡、首屏闪、数据错位——这些问题往往藏在细节里。










