1. 虚拟滚动的核心概念
定义 :虚拟滚动是一种按需渲染 技术,仅渲染可视区域内的 DOM 元素,通过占位符撑起滚动条高度,从而大幅减少 DOM 节点数量,提升性能。
为什么需要虚拟滚动?
-
性能瓶颈:渲染数万条数据时,DOM 节点过多会导致浏览器卡顿(重排、重绘、内存占用)。
-
用户体验:即使数据量巨大(如万人同屏游戏、无限加载列表),用户只能看到可视区域的内容,无需渲染所有数据。
-
适用场景:
- 表格(
table,列多且不分页) - 下拉选择器(
select,数据量大) - 列表(博客、社交媒体的无限滚动)
- 表格(
2. 静态虚拟滚动 vs 动态虚拟滚动
| 维度 | 静态虚拟滚动 | 动态虚拟滚动 |
|---|---|---|
| 子元素高度 | 固定(如 50px/项) |
不固定(如文本长度不一、图片高度动态) |
| 滚动条高度计算 | 总高度 = 数据量 × 固定高度 |
总高度 = 已渲染项的实际高度 + 未渲染项的估算高度 |
| 性能开销 | 低(高度已知,直接计算) | 高(需动态测量或估算高度) |
| 实现复杂度 | 简单(定位 + 缓冲区) | 复杂(需维护高度映射表) |
| 典型场景 | 表格、固定高度的列表 | 聊天消息、富文本列表、动态卡片 |
| 缓冲区策略 | 前后各预留 N 项(如 2 项) | 基于滚动方向动态调整缓冲区 |
3. 实现原理深度解析
3.1 静态虚拟滚动:固定高度的简化方案
核心思路:
- 撑起滚动条 :通过
min-height: 总高度创建一个"虚拟"容器,模拟所有数据的高度。 - 可视区渲染:仅渲染可视区域 + 缓冲区的数据。
- 滚动定位 :根据
scrollTop计算起始索引,通过margin-top或transform: translateY移动可视区域。
关键代码(JavaScript) :
ini
const container = document.querySelector('.container');
const ITEM_HEIGHT = 50; // 固定高度
const BUFFER = 4; // 缓冲区项数(前后各 2 项)
const showItemCount = Math.ceil(container.clientHeight / ITEM_HEIGHT); // 可视区项数
container.addEventListener('scroll', () => {
const scrollTop = container.scrollTop;
// 计算起始索引(考虑缓冲区)
const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER / 2);
// 计算可视区数据
const visibleData = originData.slice(startIndex, startIndex + showItemCount + BUFFER);
// 更新 margin-top(考虑边界)
const marginTop = startIndex * ITEM_HEIGHT;
listElement.style.marginTop = `${marginTop}px`;
// 渲染 visibleData
});
DOM 结构:
xml
<div class="container" style="height: 500px; overflow: auto;">
<div class="content" style="min-height: 500000px;"> <!-- 总高度 = 10000 × 50px -->
<div class="list" style="margin-top: 600px;"> <!-- 动态计算 -->
<div class="item">Item 9</div>
<div class="item">Item 10</div>
<!-- ... 可视区 + 缓冲区数据 -->
</div>
</div>
</div>
优点:
- 实现简单,计算高效。
- 适用于固定高度的场景(如表格行、下拉选项)。
缺点:
- 无法处理动态高度的子元素。
3.2 动态虚拟滚动:高度不固定的挑战
核心思路:
- 高度映射表 :维护一个数组
sizes,存储每个子元素的实际高度(未渲染的用估算值,如minItemSize)。 - 滚动条高度 :动态计算为
已渲染项的实际高度 + 未渲染项的估算高度。 - 滚动定位 :根据
scrollTop和sizes数组,通过二分查找快速定位起始索引。 - 高度更新 :子元素渲染后,更新
sizes并调整scrollTop补偿高度差异。
关键代码(参考 vue-virtual-scroller) :
ini
// 1. 维护高度映射表
const sizes = new Map(); // key: item.id, value: 实际高度
const minItemSize = 50; // 估算高度
// 2. 计算总高度
const totalHeight = items.reduce((sum, item) => {
return sum + (sizes.get(item.id) || minItemSize);
}, 0);
// 3. 滚动时定位起始索引(二分查找)
function findStartIndex(scrollTop) {
let low = 0, high = items.length - 1;
let mid, currentTop = 0;
while (low <= high) {
mid = Math.floor((low + high) / 2);
const itemHeight = sizes.get(items[mid].id) || minItemSize;
if (currentTop + itemHeight < scrollTop) {
currentTop += itemHeight;
low = mid + 1;
} else if (currentTop > scrollTop) {
high = mid - 1;
currentTop -= (sizes.get(items[mid].id) || minItemSize);
} else {
return mid;
}
}
return low;
}
// 4. 渲染可视区 + 缓冲区
const startIndex = findStartIndex(scrollTop);
const visibleItems = items.slice(startIndex, startIndex + visibleCount + BUFFER);
优点:
- 支持动态高度的子元素(如聊天消息、富文本)。
- 用户体验流畅(无明显跳动)。
缺点:
- 实现复杂,需维护高度映射表。
- 首次渲染时需估算高度,可能存在误差。
4. 实战:使用现有库快速集成
4.1 Vue 生态:vue-virtual-scroller
静态虚拟滚动(固定高度) :
ini
<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="32" <!-- 固定高度 -->
key-field="id"
v-slot="{ item }"
>
<div class="user">{{ item.name }}</div>
</RecycleScroller>
</template>
动态虚拟滚动(高度不固定) :
ruby
<template>
<DynamicScroller :items="items" :min-item-size="54" class="scroller">
<template v-slot="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.message]" <!-- 依赖字段,高度变化时重新计算 -->
:data-index="index"
>
<div class="message">{{ item.message }}</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
</template>
特性:
- 自动维护高度映射表。
- 支持水平/垂直滚动。
- 兼容 Vue 2/3。
4.2 表格组件:vxe-table
用法:
ini
<template>
<vxe-table
:data="tableData"
:virtual-scroll="true"
:scroll-y="{ enabled: true, gt: 100 }" <!-- 数据量 > 100 时启用虚拟滚动 -->
height="500"
>
<vxe-column field="name" title="Name"></vxe-column>
<vxe-column field="age" title="Age"></vxe-column>
</vxe-table>
</template>
特性:
- 支持上下/左右虚拟滚动。
- 适用于大数据量表格。
文档 :vxe-table
5. 性能优化建议
-
缓冲区大小:
- 静态:前后各预留 2-4 项(如
BUFFER = 4)。 - 动态:根据滚动速度动态调整(如
vue-virtual-scroller的buffer属性)。
- 静态:前后各预留 2-4 项(如
-
高度估算:
- 动态滚动中,未渲染项的高度用
minItemSize估算,渲染后更新实际高度。
- 动态滚动中,未渲染项的高度用
-
避免频繁重排:
- 使用
transform: translateY代替margin-top(触发 GPU 加速)。 - 批量更新 DOM(如
requestAnimationFrame)。
- 使用
-
内存管理:
- 回收不可见的 DOM 节点(如
vue-virtual-scroller的active属性)。
- 回收不可见的 DOM 节点(如
-
边界处理:
- 滚动到顶部/底部时,避免重复渲染。
6. 总结与选择建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 固定高度列表/表格 | 静态虚拟滚动(自实现或 vxe-table) |
简单高效,无需维护高度映射表。 |
| 动态高度列表(如聊天) | 动态虚拟滚动(vue-virtual-scroller) |
自动处理高度变化,体验流畅。 |
| React 项目 | react-window 或 react-virtualized |
类似原理,适配 React 生态。 |
| 原生 JS 项目 | 自实现静态虚拟滚动 | 代码量小,易于定制。 |
核心区别:
- 静态:高度已知 → 直接计算 → 性能最优。
- 动态:高度未知 → 需测量/估算 → 复杂但灵活。
未来趋势:
- Web Components + 虚拟滚动(如
lit-virtualizer)。 - 结合 Intersection Observer API 优化渲染时机。
7. 扩展阅读
结尾 : 虚拟滚动是前端性能优化的"杀手锏",尤其在大数据量场景下能显著提升体验。静态和动态方案各有千秋,静态适合固定高度,动态适合灵活布局 。如果不想重复造轮子,直接用 vue-virtual-scroller 或 vxe-table 就是最佳选择!
你准备在哪个场景下应用虚拟滚动?或者有没有遇到过特殊的性能挑战? 可以在评论区分享你的经验!