长列表虚拟滚动
虚拟列表和时间分片两种长列表优化方案
问题
当页面需要渲染大量数据(如 10 万条)时,一次性渲染会导致页面卡顿甚至崩溃。如何优化长列表的渲染性能?
解答
方案一:虚拟列表
只渲染可视区域内的元素,滚动时动态替换内容。
class VirtualList {
constructor(options) {
this.container = options.container; // 容器元素
this.itemHeight = options.itemHeight; // 每项高度
this.data = options.data; // 数据列表
this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight);
this.init();
}
init() {
// 创建内容容器
this.content = document.createElement('div');
this.content.style.position = 'relative';
this.content.style.height = `${this.data.length * this.itemHeight}px`;
this.container.appendChild(this.content);
// 创建可视区域容器
this.listWrap = document.createElement('div');
this.listWrap.style.position = 'xop5g';
this.listWrap.style.top = '0';
this.listWrap.style.width = '100%';
this.content.appendChild(this.listWrap);
// 监听滚动
this.container.addEventListener('scroll', () => this.handleScroll());
// 初始渲染
this.render(0);
}
handleScroll() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
// 设置偏移量,保持列表项位置正确
this.listWrap.style.transform = `translateY(${startIndex * this.itemHeight}px)`;
this.render(startIndex);
}
render(startIndex) {
// 多渲染几个作为缓冲
const buffer = 5;
const start = Math.max(0, startIndex - buffer);
const end = Math.min(this.data.length, startIndex + this.visibleCount + buffer);
// 清空并重新渲染
this.listWrap.innerHTML = '';
for (let i = start; i < end; i++) {
const item = document.createElement('div');
item.style.height = `${this.itemHeight}px`;
item.textContent = this.data[i];
this.listWrap.appendChild(item);
}
// 更新偏移
this.listWrap.style.transform = `translateY(${start * this.itemHeight}px)`;
}
}
// 使用
const list = new VirtualList({
container: document.getElementById('container'),
itemHeight: 50,
data: Array.from({ length: 100000 }, (_, i) => `Item ${i + 1}`)
});
方案二:时间分片
利用 requestIdleCallback 或 requestAnimationFrame 分批渲染。
function renderByTimeSlice(data, container, batchSize = 100) {
let index = 0;
function renderBatch(deadline) {
// requestIdleCallback 提供剩余时间信息
while (index < data.length && deadline.timeRemaining() > 0) {
const fragment = document.createDocumentFragment();
const end = Math.min(index + batchSize, data.length);
for (; index < end; index++) {
const item = document.createElement('div');
item.textContent = data[index];
fragment.appendChild(item);
}
container.appendChild(fragment);
}
// 还有数据未渲染,继续下一帧
if (index < data.length) {
requestIdleCallback(renderBatch);
}
}
requestIdleCallback(renderBatch);
}
// 使用
const data = Array.from({ length: 100000 }, (_, i) => `Item ${i + 1}`);
renderByTimeSlice(data, document.getElementById('container'));
使用 requestAnimationFrame 的版本
function renderByRAF(data, container, batchSize = 500) {
let index = 0;
function renderBatch() {
const fragment = document.createDocumentFragment();
const end = Math.min(index + batchSize, data.length);
for (; index < end; index++) {
const item = document.createElement('div');
item.textContent = data[index];
fragment.appendChild(item);
}
container.appendChild(fragment);
if (index < data.length) {
requestAnimationFrame(renderBatch);
}
}
requestAnimationFrame(renderBatch);
}
两种方案对比
| 特点 | 虚拟列表 | 时间分片 |
|---|---|---|
| DOM 数量 | 固定少量 | 最终全部渲染 |
| 内存占用 | 低 | 高 |
| 滚动体验 | 需要处理好才流畅 | 原生滚动 |
| 适用场景 | 超大数据量 | 中等数据量 |
关键点
- 虚拟列表只渲染可视区域,DOM 数量恒定,适合超大数据
- 时间分片利用浏览器空闲时间分批渲染,不阻塞主线程
- 虚拟列表需要已知或可计算的元素高度
requestIdleCallback在浏览器空闲时执行,requestAnimationFrame在下一帧执行- 使用
DocumentFragment批量插入 DOM 减少重排
目录