从原理到高并发优化实战
目录导读
- 什么是虚拟列表?为何需要它?
- 虚拟列表的核心实现原理
- 三步手写一个基础虚拟列表
- 性能优化关键点:滚动、缓存与复用
- 常见问题与解决方案(含问答)
- 企业级实践:动态高度、分组与空状态处理
什么是虚拟列表?为何需要它?
1 痛点场景
假设你需要渲染一个包含 10万条数据 的商品列表,如果直接使用 v-for 或 map 渲染全部 DOM,浏览器会:
- 卡顿数秒:页面无法滚动、点击无响应。
- 内存飙升:DOM 节点数达到数万个,占用几百 MB 内存。
- 用户体验极差:尤其移动端低端设备直接崩溃。
2 虚拟列表(Virtual List)的定义
虚拟列表 是一种只渲染 当前可视区域 内项目(加上少量缓冲区),而将不可见项目“虚拟化”为占位节点或根本不渲染的技术,它能将 DOM 节点数从 10万 降到 20-30 个,大幅提升性能。
核心公式:
渲染节点数 = 可视区域高度 / 项目平均高度 + 缓冲区数(10-20 个)
虚拟列表的核心实现原理
1 三大核心变量
- 可视区高度:列表容器的高度(如 500px)。
- 项目高度:每个列表项的高度(固定或动态)。
- 滚动位置:用户当前滚动到的
scrollTop值。
2 计算逻辑(以固定高度为例)
- 根据
scrollTop计算起始索引:startIndex = Math.floor(scrollTop / itemHeight)。 - 根据可视区高度计算结束索引:
endIndex = startIndex + Math.ceil(containerHeight / itemHeight)。 - 添加缓冲区:上下各额外渲染 3-5 个项目,防止滚动时出现白屏。
- 计算偏移量:通过
transform: translateY(offset)或padding-top,让渲染的项目始终“对准”滚动位置。
数据结构示意:
[
{ id: 1, content: '项目1', height: 50 },
{ id: 2, content: '项目2', height: 50 },
...
]
三步手写一个基础虚拟列表(附代码逻辑)
1 第一步:HTML 结构
<div id="list-container" style="height: 400px; overflow-y: auto;">
<div id="list-phantom" style="height: 500000px;"> <!-- 总高度占位 -->
<div id="list-content" style="transform: translateY(0);">
<!-- 实时渲染的 20 个实际项目 -->
</div>
</div>
</div>
- phantom(幻影层):设置
height = 总数据量 * 每项高度,产生正常的滚动条。 - content(内容层):通过
translateY控制实际渲染位置的偏移。
2 第二步:核心 JS 逻辑
const container = document.getElementById('list-container');
const phantom = document.getElementById('list-phantom');
const content = document.getElementById('list-content');
const totalItems = 100000;
const itemHeight = 50;
let startIndex = 0;
let endIndex = 0;
phantom.style.height = totalItems * itemHeight + 'px';
function render(scrollTop) {
const visibleCount = Math.ceil(container.clientHeight / itemHeight);
startIndex = Math.floor(scrollTop / itemHeight);
endIndex = startIndex + visibleCount + 5; // 多渲染 5 个作为缓冲区
// 生成当前需要渲染的项目数据
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex && i < totalItems; i++) {
const div = document.createElement('div');
div.style.height = itemHeight + 'px';
div.textContent = `项目 ${i}`;
fragment.appendChild(div);
}
content.innerHTML = '';
content.appendChild(fragment);
content.style.transform = `translateY(${startIndex * itemHeight}px)`;
}
container.addEventListener('scroll', () => {
render(container.scrollTop);
});
// 首次加载
render(0);
3 第三步:处理边界情况
- 当
startIndex < 0时设为 0。 - 当
endIndex > totalItems时设为totalItems。 - 确保
scrollTop不会导致索引溢出。
性能优化关键点:滚动、缓存与复用
1 滚动节流(Throttle)
问题:scroll 事件触发频率极高,每次调用 render 可能导致重排(Reflow)。
优化:使用 requestAnimationFrame 或 16ms 的节流。
let ticking = false;
container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
render(container.scrollTop);
ticking = false;
});
ticking = true;
}
});
2 项目复用与差分更新
- 复用 DOM:不要每次
innerHTML = ''再append,维护一个“项目池”,只更新数据和样式。 - 虚拟 DOM 对比:使用
IntersectionObserver或手动 diff 来精准增删。
3 动态高度处理
当项目高度不固定时(如聊天记录),需要:
- 预估高度:首次渲染使用预估高度(如 50px)。
- 测量真实高度:渲染后通过
getBoundingClientRect()获取实际高度,存入高度数组。 - 重新计算偏移:用累计高度数组
cumulativeHeight替代startIndex * itemHeight。
核心算法(高度缓存):
const heights = new Array(totalItems).fill(0);
const cumulativeHeights = [0]; // cumulativeHeights[i] = 前 i 项总高度
function updateCumulative() {
for (let i = 1; i <= totalItems; i++) {
cumulativeHeights[i] = cumulativeHeights[i-1] + (heights[i-1] || estimatedHeight);
}
}
常见问题与解决方案(以问答形式)
Q1:虚拟列表会导致顶部或底部出现白屏?
A:原因是缓冲区不足或渲染不及时。解决方案:
- 增加缓冲区数量(上下各 10-15 个)。
- 使用
will-change: transform让浏览器开启 GPU 加速。 - 确保
scroll事件的处理函数执行时间低于 16ms。
Q2:如何让虚拟列表支持“滚动到指定位置”?
A:使用 scrollTo 方法,计算对应索引的 scrollTop:
function scrollToIndex(index) {
const top = cumulativeHeights[index] || index * estimatedHeight;
container.scrollTo({ top, behavior: 'smooth' });
}
Q3:虚拟列表中的图片加载导致滚动闪烁?
A:图片加载会改变项目高度,导致偏移错乱。解决方案:
- 为图片预留固定宽高占位(推荐)。
- 使用
IntersectionObserver仅在图片进入可视区时加载。 - 图片加载完成后触发重新计算高度。
Q4:如何实现“分组表头”或“粘性头部”?
A:将分组视为特殊项目,高度单独计算,粘性头部可通过 position: sticky 实现,但需注意:
- 粘性头部在虚拟列表中子项目容易错位,建议将粘性头部作为独立层覆盖在内容层之上。
- 当滚动到某个分组时,上方出现一个固定头部,内容继续滚动。
企业级实践:动态高度、分组与空状态处理
1 空状态与加载占位
- 当数据为 0 时:不渲染任何项目,显示“暂无数据”。
- 首次加载时:使用骨架屏(Skeleton)作为占位,高度设为预估高度。
2 内存泄漏预防
- 确保
removeEventListener被调用(例如使用AbortController)。 - 使用
WeakMap存储 DOM 引用,避免闭包导致的项目对象无法回收。
3 实际项目案例
某电商平台使用虚拟列表实现商品瀑布流:
- 数据量:50万条商品。
- 性能指标:首次渲染时间 < 1s,滚动帧率稳定 60fps。
- 技术栈:Vue +
@tanstack/virtual(原 react-virtual,现跨框架)。
虚拟列表实现的“黄金法则”
| 场景 | 实现策略 |
|---|---|
| 固定高度 | 直接用 第3节 的代码 |
| 动态高度 | 需维护高度数组,滚动时重新计算累计高度 |
| 海量数据 + 高帧率 | 加节流、复用 DOM、使用 Web Worker 计算偏移量 |
| 移动端低端设备 | 减少缓冲区、使用 scroll 事件但不频繁渲染 |
最后一句:虚拟列表的核心是“以有限 DOM 模拟无限数据”,掌握了这一思想,你能轻松适应任何框架(React、Vue、原生 JS)的实现。