虚拟列表如何避免Layout和Paint
虚拟列表(Virtual List / Windowing)的核心价值,本质就是:
👉 让浏览器“以为”列表很大,但实际上参与 Layout / Paint 的节点极少
# 一、虚拟列表到底在“避免”什么?
# 如果不用虚拟列表(灾难现场)
10,000 条 DOM
↓
每次滚动:
- Layout(全部参与)
- Paint(大量节点)
- Style Recalc
1
2
3
4
5
6
2
3
4
5
6
📉 FPS 直接崩
# 使用虚拟列表后
可视区 20 条 + buffer 10 条 = 30 条 DOM
↓
滚动:
- 少量 transform
- 少量 style 更新
- 几乎不 Layout
1
2
3
4
5
6
2
3
4
5
6
✔️ 性能线性 → 常数级
# 二、虚拟列表的三大“避坑点”(核心原理)
# ① DOM 数量控制(最重要)
Layout / Paint 的复杂度 ≈ DOM 数量
虚拟列表只渲染:
[ startIndex, endIndex ]
1
其余:
- 不存在 DOM
- 不参与 Render Tree
- 不参与 Layout / Paint
# ② 滚动用“位移”而不是“重排”
# ❌ 错误做法(会 Layout)
item.style.top = index * itemHeight + 'px'
1
- top → 影响布局
- 每个 item 都可能触发布局计算
# ✅ 正确做法(只 Composite)
container.style.transform = `translateY(${offset}px)`
1
- transform → 合成层
- 跳过 Layout / Paint
📌 这是虚拟列表流畅的关键
# ③ 占位但不渲染(scroll illusion)
# 核心结构
<div class="viewport">
<div class="phantom"></div>
<div class="content"></div>
</div>
1
2
3
4
2
3
4
.viewport {
overflow-y: auto;
height: 400px;
}
.phantom {
height: 100000px; /* 总高度 */
}
.content {
position: absolute;
transform: translateY(2000px);
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 浏览器视角
- phantom:只参与一次 Layout
- content:少量 DOM + transform
👉 滚动条真实存在,但 DOM 极少
# 三、完整渲染流程对比(非常重要)
# 普通列表滚动
scroll
→ Layout(全部子节点)
→ Paint(大量)
→ Composite
1
2
3
4
2
3
4
# 虚拟列表滚动
scroll
→ JS 计算 startIndex
→ transform 位移
→ Composite
1
2
3
4
2
3
4
📌 中间直接跳过 Layout / Paint
# 四、为什么滚动本身不会频繁 Layout?
# 浏览器优化点
滚动 ≠ 重新计算布局
只是:
- 改变 scroll offset
- 裁剪可视区域
# 但问题在于:
👉 子节点太多时,Paint 和 Composite 还是贵
虚拟列表直接:
- 不让它们存在
# 五、变高列表(Variable Height)怎么办?
这是进阶难点。
# 方案一:固定高度(最快)
- 最佳性能
- 实现最简单
# 方案二:高度缓存(主流方案)
heightMap[index] = realHeight
1
- 首次测量
- 后续直接用缓存
- 避免反复
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 + 用合成层
# 八、一个极简“虚拟列表核心算法”(理解用)
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))
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 九、总结
- 虚拟列表通过只渲染可视区附近的少量 DOM,
- 大幅减少参与 Layout 和 Paint 的节点数量。
- 同时使用 transform 进行位移,
- 让滚动过程主要发生在 Composite 阶段,
- 从而在大数据量列表中保持稳定的渲染性能。
上次更新: 2026/01/07, 09:20:46