虚拟列表(Virtual List / Windowing)的核心价值,本质就是:
👉 让浏览器“以为”列表很大,但实际上参与 Layout / Paint 的节点极少
一、虚拟列表到底在“避免”什么?
如果不用虚拟列表(灾难现场)
10,000 条 DOM
↓
每次滚动:
- Layout(全部参与)
- Paint(大量节点)
- Style Recalc📉 FPS 直接崩
使用虚拟列表后
可视区 20 条 + buffer 10 条 = 30 条 DOM
↓
滚动:
- 少量 transform
- 少量 style 更新
- 几乎不 Layout✔️ 性能线性 → 常数级
二、虚拟列表的三大“避坑点”(核心原理)
① DOM 数量控制(最重要)
Layout / Paint 的复杂度 ≈ DOM 数量
虚拟列表只渲染:
[ startIndex, endIndex ]其余:
- 不存在 DOM
- 不参与 Render Tree
- 不参与 Layout / Paint
② 滚动用“位移”而不是“重排”
❌ 错误做法(会 Layout)
js
item.style.top = index * itemHeight + 'px'- top → 影响布局
- 每个 item 都可能触发布局计算
✅ 正确做法(只 Composite)
js
container.style.transform = `translateY(${offset}px)`- transform → 合成层
- 跳过 Layout / Paint
📌 这是虚拟列表流畅的关键
③ 占位但不渲染(scroll illusion)
核心结构
html
<div class="viewport">
<div class="phantom"></div>
<div class="content"></div>
</div>css
.viewport {
overflow-y: auto;
height: 400px;
}
.phantom {
height: 100000px; /* 总高度 */
}
.content {
position: absolute;
transform: translateY(2000px);
}浏览器视角
- phantom:只参与一次 Layout
- content:少量 DOM + transform
👉 滚动条真实存在,但 DOM 极少
三、完整渲染流程对比(非常重要)
普通列表滚动
scroll
→ Layout(全部子节点)
→ Paint(大量)
→ Composite虚拟列表滚动
scroll
→ JS 计算 startIndex
→ transform 位移
→ Composite📌 中间直接跳过 Layout / Paint
四、为什么滚动本身不会频繁 Layout?
浏览器优化点
滚动 ≠ 重新计算布局
只是:
- 改变 scroll offset
- 裁剪可视区域
但问题在于:
👉 子节点太多时,Paint 和 Composite 还是贵
虚拟列表直接:
- 不让它们存在
五、变高列表(Variable Height)怎么办?
这是进阶难点。
方案一:固定高度(最快)
- 最佳性能
- 实现最简单
方案二:高度缓存(主流方案)
js
heightMap[index] = realHeight- 首次测量
- 后续直接用缓存
- 避免反复
getBoundingClientRect
📌 防止 Layout Thrashing
方案三:IntersectionObserver(现代方案)
- 非同步测量
- 减少强制 Layout
- 异步感知可见性
六、DevTools 怎么验证“真的没 Layout / Paint”?
1️⃣ Performance 面板
滚动过程中:
- 几乎无 Layout
- 少量 Composite
2️⃣ Paint flashing
- 可视区域极少闪烁
3️⃣ FPS meter
- 稳定 60
七、React / Vue 为什么都推荐虚拟列表?
React(react-window)
- 使用 transform
- 合并 setState
- rAF 调度
Vue(vue-virtual-scroller)
- 绝对定位 + translate
- 缓存高度
- 减少 watcher 触发
👉 本质都是:
控制 DOM + 用合成层
八、一个极简“虚拟列表核心算法”(理解用)
js
const itemHeight = 40
const visibleCount = Math.ceil(viewportHeight / itemHeight)
function onScroll(scrollTop) {
const start = Math.floor(scrollTop / itemHeight)
const end = start + visibleCount + buffer
content.style.transform =
`translateY(${start * itemHeight}px)`
render(items.slice(start, end))
}九、总结
- 虚拟列表通过只渲染可视区附近的少量 DOM,
- 大幅减少参与 Layout 和 Paint 的节点数量。
- 同时使用 transform 进行位移,
- 让滚动过程主要发生在 Composite 阶段,
- 从而在大数据量列表中保持稳定的渲染性能。