大数据列表处理
使用虚拟滚动和时间分片处理十万条数据渲染
问题
后端一次性返回十万条数据,直接渲染会导致页面卡顿甚至崩溃,如何优化?
解答
两种主流方案:虚拟滚动和时间分片。
方案一:虚拟滚动
只渲染可视区域内的元素,滚动时动态替换内容。
<!DOCTYPE html>
<html>
<head>
<style>
.container {
height: 500px;
overflow-y: auto;
position: relative;
}
.phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
}
.list {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.item {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #eee;
padding: 0 10px;
}
</style>
</head>
<body>
<div class="container" id="container">
<!-- 撑开滚动高度的占位元素 -->
<div class="phantom" id="phantom"></div>
<!-- 实际渲染的列表 -->
<div class="list" id="list"></div>
</div>
<script>
class VirtualList {
constructor(options) {
this.container = options.container;
this.phantom = options.phantom;
this.list = options.list;
this.data = options.data;
this.itemHeight = options.itemHeight;
// 可视区域能显示的数量
this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight);
// 缓冲区数量,防止滚动时出现空白
this.bufferCount = 5;
this.init();
}
init() {
// 设置占位元素高度
this.phantom.style.height = this.data.length * this.itemHeight + 'px';
// 初始渲染
this.render(0);
// 监听滚动
this.container.addEventListener('scroll', () => {
const scrollTop = this.container.scrollTop;
this.render(scrollTop);
});
}
render(scrollTop) {
// 计算起始索引
let startIndex = Math.floor(scrollTop / this.itemHeight);
startIndex = Math.max(0, startIndex - this.bufferCount);
// 计算结束索引
let endIndex = startIndex + this.visibleCount + this.bufferCount * 2;
endIndex = Math.min(this.data.length, endIndex);
// 偏移量,让列表对齐到正确位置
const offset = startIndex * this.itemHeight;
this.list.style.transform = `translateY(${offset}px)`;
// 渲染可见项
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = this.data[i];
fragment.appendChild(div);
}
this.list.innerHTML = '';
this.list.appendChild(fragment);
}
}
// 生成十万条数据
const data = Array.from({ length: 100000 }, (_, i) => `第 ${i + 1} 条数据`);
// 初始化虚拟列表
new VirtualList({
container: document.getElementById('container'),
phantom: document.getElementById('phantom'),
list: document.getElementById('list'),
data: data,
itemHeight: 50
});
</script>
</body>
</html>
方案二:时间分片
利用 requestIdleCallback 或 requestAnimationFrame 分批渲染,避免长时间阻塞主线程。
<!DOCTYPE html>
<html>
<head>
<style>
.container {
height: 500px;
overflow-y: auto;
}
.item {
height: 30px;
line-height: 30px;
border-bottom: 1px solid #eee;
padding: 0 10px;
}
</style>
</head>
<body>
<div class="container" id="container"></div>
<script>
// 生成十万条数据
const data = Array.from({ length: 100000 }, (_, i) => `第 ${i + 1} 条数据`);
const container = document.getElementById('container');
// 方式一:使用 requestIdleCallback
function renderByIdleCallback(data, container) {
const chunkSize = 200; // 每批渲染数量
let currentIndex = 0;
function render(deadline) {
// 当前帧有空闲时间且还有数据未渲染
while (deadline.timeRemaining() > 0 && currentIndex < data.length) {
const fragment = document.createDocumentFragment();
const end = Math.min(currentIndex + chunkSize, data.length);
for (let i = currentIndex; i < end; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = data[i];
fragment.appendChild(div);
}
container.appendChild(fragment);
currentIndex = end;
}
// 还有数据,继续请求下一次空闲回调
if (currentIndex < data.length) {
requestIdleCallback(render);
}
}
requestIdleCallback(render);
}
// 方式二:使用 requestAnimationFrame
function renderByRAF(data, container) {
const chunkSize = 500; // 每帧渲染数量
let currentIndex = 0;
function render() {
const fragment = document.createDocumentFragment();
const end = Math.min(currentIndex + chunkSize, data.length);
for (let i = currentIndex; i < end; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = data[i];
fragment.appendChild(div);
}
container.appendChild(fragment);
currentIndex = end;
// 还有数据,继续下一帧渲染
if (currentIndex < data.length) {
requestAnimationFrame(render);
}
}
requestAnimationFrame(render);
}
// 使用 requestAnimationFrame 方式
renderByRAF(data, container);
</script>
</body>
</html>
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 虚拟滚动 | DOM 数量少,性能好 | 实现复杂,不定高度处理麻烦 | 长列表展示 |
| 时间分片 | 实现简单,保留完整 DOM | 最终 DOM 数量大,内存占用高 | 一次性渲染后不再变化 |
关键点
- 虚拟滚动只渲染可视区域,通过计算
startIndex和endIndex动态更新内容 - 需要设置缓冲区(buffer)防止快速滚动时出现空白
- 时间分片利用浏览器空闲时间分批渲染,不阻塞用户交互
requestIdleCallback在空闲时执行,requestAnimationFrame每帧执行一次- 实际项目推荐使用成熟库:
react-window、vue-virtual-scroller
目录