1. 虚拟列表
1.1. 虚拟列表的原理?
- 虚拟列表是一种优化长列表渲染性能的技术,通过只渲染可视区域的内容,而不是整个列表,从而提高渲染性能和用户体验
1.2. 为什么需要虚拟列表?
- 性能瓶颈:
- 数据量过大时,直接渲染所有DOM 节点会导致页面性能下降,甚至出现卡顿或者崩溃现象。
- 包括浏览器的重绘和重排可能都是会造成性能问题的
- 解决目标:
- 只渲染可视区域的内容 动态复用不可见的DOM元素
1.3. 虚拟列表的核心思想
- 实现方式
- 仅渲染可视区域的内容
- 列表中不可见的部分不会生成对应的 DOM 节点
- 动态复用不可见的 DOM 元素
- 当列表滚动时,只更新可视区域的 DOM 节点,而不是重新渲染整个列表。
- 占位元素
- 通过一个大的占位容器,确保滚动条的正常使用,在列表的顶部和底部添加占位元素,用于计算可视区域的起始索引和结束索引。
- 仅渲染可视区域的内容
1.4. 实现步骤
1.4.1. 确定可见区域的的高度和可渲染的数量
- 假设容器高度为
containerHeight
。 - 每个列表项的固定高度为
itemHeight
。 - 可视区域内可以显示的列表数量为
let visibleCount = Math.ceil(containerHeight / itemHeight);
- 渲染的额外缓冲区数量为
buffer
, 保证滚动的流畅度
let renderCount = visibleCount + 2 * buffer
1.4.2. 动态计算开始渲染的起点
- 根据滚动位置计算可见列表的其实索引
startIndex
let startIndex = Math.floor(scrollTop / itemHeight);
- 根据
startIndex
和renderCount
,可以确定需要渲染的列表项索引范围
let endIndex = startIndex + renderCount - 1;
1.4.3. 占位元素
- 创建一个占位元素,它的高度等于整个列表的高度
let totalHeight = totalItemCount * itemHeight;
通过设置占位元素的高度,确保滚动条正常工作
1.4.4. 渲染可见区域的内容
- 通过偏移量
startIndex
对应元素的位置
let offsetY = startIndex * itemHeight;
1.5. 原生代码实现
1.5.1. HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>虚拟列表</title>
<style>
#container {
position: relative; /* 新增定位上下文 */
height: 400px;
overflow-y: auto;
border: 1px solid #000;
}
#list {
position: absolute; /* 脱离文档流 */
top: 0;
left: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="container" >
<div id="placeholder"></div>
<div id="list"></div>
</div>
<script src="./VirtualList.js"></script>
</body>
</html>
1.5.2. JavaScript
const container = document.getElementById('container'); // 容器
const placeholder = document.getElementById('placeholder'); // 占位符
const list = document.getElementById('list'); // 列表
const totalItems = 10000; // 总数据量
const itemHeight = 30; // 每个数据项的高度
const containerHeight = container.clientHeight; // 容器高度
const visibleItems = Math.ceil(containerHeight / itemHeight); // 可见数据项数量
const buffer = 5; // 缓冲区数量
const renderCount = visibleItems + buffer * 2; // 渲染数据项数量
// 模式生成10000个数据
const data = Array.from({ length: totalItems }, (_, index) => ({
id: index,
value: `Item ${index}`,
}));
// 设置占位容器的高度
placeholder.style.height = `${totalItems * itemHeight}px`;
// 渲染可视区域列表
function renderVirtualList(scrollTop) {
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
const endIndex = Math.min(totalItems -1, startIndex + renderCount - 1);
// 计算偏移量
const offsetY = startIndex * itemHeight;
list.style.transform = `translateY(${offsetY}px)`;
// 渲染可见内容
list.innerHTML = data.slice(startIndex, endIndex + 1).map(item => `<div style="color: red; height: ${itemHeight}px">${item.value}</div>`).join('');
}
// 初始化渲染
container.addEventListener('scroll', () => {
const scrollTop = container.scrollTop;
renderVirtualList(scrollTop);
});
renderVirtualList(0);
1.6. 核心原理解析
1.6.1. 滚动事件驱动
- 监听滚动事件,计算当前的 scrollTop, 动态确定需要渲染的
startIndex
和endIndex
- 滚动到新位置时,更新可视区域的 DOM 内容。
1.6.2. 占位容器
- 占位元素通过设置总高度,维持滚动条的正确性。
- 真实 DOM 仅渲染可见部分,降低 DOM 节点数量
1.6.3. DOM 元素复用
- 滚动时并不会移除和新建大量 DOM 节点,而是复用现有 DOM,动态更新内容
1.7. 优化
1.7.1. 优化点 1 DOM 渲染延迟 出现白屏现象
1.7.1.1. 问题描述
- 当用户快速滚动的时候
scroll
事件频繁触发,浏览器可能无法及时完成DOM
的更新,导致用户看到空白区域或闪烁
1.7.1.2. 原因
渲染滞后
:快速滚动时,scroll
事件触发评率过高,渲染任务无法跟上滚动的速度。计算复杂
: 每次滚动都需要重新极端可见区域的索引范围,并生成新的 DOM 内容
1.7.1.3. 解决方案 1 节流 | 防抖
let ticking = false;
container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
renderVirtualList(container.scrollTop);
ticking = false;
});
ticking = true;
}
});
- 使用
requestAnimationFrame
或throttle
降低scroll
事件触发的频率
1.7.1.4. 解决方案 2 预渲染
- 渲染当前可见范围之外的额外缓冲区域(如上下各增加 2-3 屏),减少快速滚动时的白屏问题
- 简单说就是 提前多渲染一部分内容 做为缓冲区
1.7.2. 优化点 2 滚动跳跃(滚动不流畅)
1.7.2.1. 问题描述
- 快速滚动时,可能出现内容突然跳跃或位置错乱的情况,尤其是在列表项高度动态变化的情况下
1.7.2.2. 原因
列表项高度动态变化
:虚拟列表通常假设列表项高度固定,若高度动态变化,会导致滚动位置计算错误。滚动位置重置
:快速滑动时,渲染延迟或者数据更新不同步,可能导致滚动位置被重置。
1.7.2.3. 解决方案 1 固定高度
- 确保列表项的高度固定,避免动态高度。 提前计算要渲染项列表每项的高度进行累加
- 如果高度动态变化,提前计算每个项的高度,使用累加值管理滚动位置。
const itemHeights = [....30, 40, 50]; // 每一项的高度
const offsetY = itemHeights.slice(0, startIndex).reduce((sum, h) => sum + h, 0)
1.7.2.4. 解决方案 2 缓存布局
- 对于高度动态的场景,提前缓存已计算的高度,减少重复计算。
const heightCache = new Map();
function getItemHeight(index) {
if (!heightCache.has(index)) {
const height = calculateHeight(index); // 动态计算高度
heightCache.set(index, height);
}
return heightCache.get(index);
}
1.7.2.5. 解决方案 3 平滑滚动
- 在渲染时保持视图与滚动位置同步,避免视觉上的跳跃。
1.7.3. 优化点 3 首屏渲染卡顿
1.7.3.1. 问题描述
- 当虚拟列表初始化时,如果数据量非常大(如上万条),可能会导致首屏渲染时间较长,用户体验不佳
1.7.3.2. 原因
初始化计算开销大
:需要计算整个列表的占位高度,并生成首屏可见的 DOM 节点。占位 DOM 的创建
:虚拟列表会创建一个大的占位元素,初始化开销较高
1.7.3.3. 解决方案 1 延迟加载
- 初始化时仅加载首屏内容,其他部分可以异步加载
setTimeout(() => {
renderVirtualList(container.scrollTop);
}, 5)
1.7.3.4. 解决方案 2 异步数据分批渲染
- 对数据进行分批加载而不是一次性加载完
const batchSize = 100;
for (let i = 0; i < totalItems; i += batchSize) {
setTimeout(() => renderBatch(i, i + batchSize), 0);
}
1.8. 其他方式
- 还有很多便捷的方式 就是借助三方插件库
1.9. 总结
1.9.1. 快速滑动常见问题
- DOM 渲染延迟:导致白屏或闪烁。
- 动跳跃:由于动态高度或同步问题导致位置错乱。
- 首屏卡顿:初始化计算过多。
- 滚动条错位:占位高度不准确或不同步。
- 内存占用过大:极大数据量导致性能瓶颈