虚拟列表非万能解法,其核心是仅渲染视口及缓冲区节点并用高度占位;intersectionobserver无法替代,因不控dom数量且缺位置映射;优先用react-window等成熟库,自研需注意滚动容器样式、索引映射用二分查找及图片懒加载等细节。

虚拟列表不是用来“解决长列表渲染卡顿”的银弹,它和长列表本身不冲突,但用错场景或实现不当,反而会让长列表更卡、更难维护。
为什么直接渲染几千个 <div> 会卡
<p>浏览器渲染性能瓶颈不在 JS,而在 DOM 树深度和 layout/reflow 频次。哪怕每个 <code><div> 只有文字,10000 项也会触发:
<ul>
<li>内存占用飙升(每个 DOM 节点约 1–2KB)</li>
<li>首次 render 延迟明显(V8 解析 + 渲染树构建 + layout)</li>
<li>滚动时频繁触发 <code>scroll 事件 + getBoundingClientRect 或 offsetTop 计算,容易掉帧
虚拟列表的核心思路是:只渲染视口内 + 少量缓冲区的节点(比如 20 个),其余用 height 占位。这要求你放弃“一次性全量挂载”的惯性思维。
IntersectionObserver 能替代虚拟列表吗
不能。它适合做懒加载或曝光统计,但无法解决长列表的渲染密度问题:
-
IntersectionObserver不控制 DOM 数量,只是监听可见性 —— 你仍得提前挂载全部节点(或大量预加载) - 滚动快速时,observer 回调可能延迟或合并,导致占位高度错乱、白屏或闪烁
- 无滚动位置映射逻辑,无法支持
scrollTop精确跳转(如点击目录跳到第 5000 行)
真要用 observer,也得配合动态卸载 + 高度缓存 + 位置索引,这已接近手写虚拟列表的复杂度。
React/Vue 中用 react-window 或 vue-virtual-scroller 还是自己写
优先用成熟库,除非你明确需要定制化行为(比如混合不同高度 item、嵌套滚动、服务端渲染兼容):
-
react-window默认假设所有 item 等高,用FixedSizeList;变高需换VariableSizeList,且必须提前提供getItemSize(index)—— 没缓存会反复计算,拖慢滚动 -
vue-virtual-scroller的RecycleList对 slot 编译有约束,父子组件通信若依赖v-model或ref,容易因节点复用失效 - 自己写要注意:滚动容器必须设
overflow-y: auto且有固定高度;scrollTop和数据索引映射必须用二分查找(不能线性遍历),否则 10w 条数据下跳转会卡死
虚拟列表里图片加载和骨架屏怎么处理
这是最容易被忽略的性能断点 —— 虚拟列表只管 DOM 数量,不管资源加载:
- 图片不加
loading="lazy"或不走 IntersectionObserver 懒加载,首屏滚动仍会触发几十张图并发请求 - 骨架屏若用真实 DOM 实现(比如每个 item 渲染 3 个
<div class="skeleton">),就违背了虚拟化本意;应统一用单个占位 <code><div class="item-placeholder"> + CSS 骨架动画 <li>服务端返回的数据若含 base64 图片,务必在虚拟列表外做裁剪/降质,避免 JS 解析大字符串阻塞主线程</li> <p>真正难的不是“怎么让列表不卡”,而是“怎么让滚动中每帧都可控”——高度缓存是否准确、滚动事件是否节流得当、DOM 复用是否破坏状态,这些细节一错,虚拟列表就变成伪优化。</p> </div>











