你在开发后台管理系统时,是否遇到过这样的场景:
后端返回了 10 万条数据,你用 v-for 或 map 直接渲染到页面上,结果浏览器直接卡死,鼠标都动不了。
为什么?因为浏览器要同时创建 10 万个 DOM 节点,内存爆炸、重排重绘频繁触发,页面直接"休克"。
今天,我们用算法来解决这个问题——虚拟列表(Virtual List) 。它的核心思想是:只渲染用户看得见的部分。
1. 虚拟列表的核心原理
想象一下你在坐高铁,窗外有 1000 根电线杆。你看到的永远是眼前的几根,而不是全部。虚拟列表就是这个道理:
- 完整数据:10 万条(存在内存中,不渲染)
- 可视区域:屏幕能显示的约 10 条(只渲染这部分 DOM)
- 滚动时:动态计算应该显示哪几条,替换 DOM 内容
性能对比
| 方案 | DOM 节点数 | 内存占用 | 滚动流畅度 |
|---|---|---|---|
| 直接渲染 | 100,000 | 500MB+ | 卡顿到无法使用 |
| 虚拟列表 | 15-20 | 2MB | 丝滑如德芙 |
2. 核心算法:如何知道该渲染哪几条?
这是虚拟列表最关键的部分。我们需要根据滚动位置计算出起始索引和结束索引。
function getVisibleRange(scrollTop, containerHeight, itemHeight, bufferCount) {
// 计算起始索引(向上扩展缓冲区)
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - bufferCount
);
// 计算结束索引(向下扩展缓冲区)
const endIndex = Math.min(
totalItems - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + bufferCount
);
return { startIndex, endIndex };
}
算法解析
Math.floor(scrollTop / itemHeight):计算出当前滚动到了第几项bufferCount:缓冲区,防止滚动过快时出现白屏(通常设 3-5)Math.max(0, ...)和Math.min(total - 1, ...):边界保护,防止索引越界
3. 手写一个完整的虚拟列表
我们来实现一个不依赖任何框架的纯 JavaScript 版本:
class VirtualList {
constructor(container, options) {
this.container = container;
this.itemHeight = options.itemHeight; // 每项高度
this.data = options.data; // 完整数据
this.bufferCount = options.bufferCount || 5; // 缓冲区
// 计算可视区域能显示多少项
this.visibleCount = Math.ceil(container.clientHeight / this.itemHeight);
this.init();
}
init() {
// 创建一个"占位容器",高度等于所有数据的总高度
// 这样滚动条才能正确反映数据量
this.scrollContainer = document.createElement('div');
this.scrollContainer.style.height = `${this.data.length * this.itemHeight}px`;
this.scrollContainer.style.position = 'relative';
this.scrollContainer.style.overflowY = 'auto';
// 创建实际渲染的容器
this.visibleContainer = document.createElement('div');
this.visibleContainer.style.position = 'absolute';
this.visibleContainer.style.top = '0';
this.visibleContainer.style.width = '100%';
this.scrollContainer.appendChild(this.visibleContainer);
this.container.appendChild(this.scrollContainer);
// 监听滚动事件
this.scrollContainer.addEventListener('scroll', () => {
this.render();
});
// 初始渲染
this.render();
}
render() {
const scrollTop = this.scrollContainer.scrollTop;
const { startIndex, endIndex } = this.getVisibleRange(scrollTop);
// 计算偏移量(让滚动看起来是连续的)
const offsetY = startIndex * this.itemHeight;
this.visibleContainer.style.transform = `translateY(${offsetY}px)`;
// 清空并重新渲染可视区域内的项
this.visibleContainer.innerHTML = '';
for (let i = startIndex; i <= endIndex; i++) {
const item = document.createElement('div');
item.style.height = `${this.itemHeight}px`;
item.textContent = this.data[i];
this.visibleContainer.appendChild(item);
}
}
}
4. 工业界实战:不定高虚拟列表
上面的实现假设每项高度固定。但实际项目中,列表项高度可能不一致(如朋友圈动态)。
解决方案:
- 预估高度:给每项一个初始预估高度
- 动态测量:渲染后用
ResizeObserver测量真实高度 - 修正偏移:根据真实高度重新计算位置
这是 Vue Virtual Scroller 和 React Window 等成熟库的做法。
5. 面试考点
Q1: 虚拟列表和普通列表的性能差异在哪里?
A: 普通列表渲染 N 个 DOM,虚拟列表只渲染可视区域的 M 个(M << N)。时间复杂度从 O(N) 降到 O(M)。
Q2: 为什么需要 bufferCount(缓冲区)?
A: 防止用户滚动过快时,新内容还没渲染出来就出现了白屏。缓冲区提前渲染了屏幕外的几项。
Q3: 如何处理动态高度的列表项?
A: 使用 ResizeObserver 监听元素高度变化,动态维护一个高度数组,重新计算每项的位置偏移。
6. 总结
虚拟列表不是什么神秘的"黑科技",它只是一个聪明的数学计算 + DOM 复用的组合拳:
- 计算可视范围:根据滚动位置算出应该显示哪些项
- 占位容器:用一个大 div 撑起完整的高度,让滚动条正常工作
- 动态渲染:只创建和更新屏幕内的 DOM 节点
下次遇到长列表卡顿,别再傻乎乎地直接渲染了,用虚拟列表,性能提升几个数量级!
💡 提示: 完整代码已开源到 GitHub,欢迎 Star 支持!在公众号【Lee 的成长日记】回复"虚拟列表",可获取可运行的 HTML Demo 文件。
如果你觉得这篇关于"前端性能优化"的文章对你有帮助,欢迎点赞收藏!🚀