前端项目中如果存在万级甚至十万级数据的长列表场景,直接将全部数据渲染为DOM节点会导致浏览器渲染压力过大,出现页面卡顿、滚动掉帧等问题。虚拟滚动通过只渲染可视区域内的列表项,动态计算需要展示的数据区间,复用已有的DOM节点,能够大幅降低渲染开销,提升页面性能。
虚拟滚动核心原理
虚拟滚动的核心逻辑可以总结为三个步骤:
- 计算容器可视区域的高度和每个列表项的高度,得到可视区域能展示的列表项数量
- 监听容器的滚动事件,根据滚动偏移量计算当前需要展示的数据起始索引和结束索引
- 只渲染起始索引到结束索引之间的数据,同时调整列表容器的总高度,保证滚动条行为符合预期
方案一:固定高度虚拟滚动(基础版)
适用于所有列表项高度固定的场景,实现逻辑最简单,性能也最优。
实现代码
// 容器元素
const container = document.getElementById('container');
// 列表总数据
const data = Array.from({ length: 10000 }, (_, i) => `列表项 ${i + 1}`);
// 单个列表项高度
const itemHeight = 50;
// 容器可视高度
const containerHeight = container.clientHeight;
// 可视区域可展示的列表项数量
const visibleCount = Math.ceil(containerHeight / itemHeight);
// 列表总高度
const totalHeight = data.length * itemHeight;
// 创建总高度占位容器
const phantom = document.createElement('div');
phantom.style.height = `${totalHeight}px`;
phantom.style.position = 'relative';
container.appendChild(phantom);
// 创建实际渲染内容的容器
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.left = '0';
content.style.top = '0';
content.style.right = '0';
phantom.appendChild(content);
// 初始渲染
function renderContent() {
const scrollTop = container.scrollTop;
// 计算起始索引
const startIndex = Math.floor(scrollTop / itemHeight);
// 计算结束索引,多渲染2项避免滚动时出现空白
const endIndex = Math.min(startIndex + visibleCount + 2, data.length);
// 设置内容容器的偏移量
content.style.transform = `translateY(${startIndex * itemHeight}px)`;
// 渲染对应区间的数据
let html = '';
for (let i = startIndex; i < endIndex; i++) {
html += `<div class="list-item" style="height:${itemHeight}px;line-height:${itemHeight}px">${data[i]}</div>`;
}
content.innerHTML = html;
}
// 监听滚动事件
container.addEventListener('scroll', renderContent);
// 初始渲染
renderContent();
方案二:动态高度虚拟滚动
适用于列表项高度不固定的场景,需要额外维护每个列表项的高度和位置信息。
实现代码
const container = document.getElementById('container');
const data = Array.from({ length: 10000 }, (_, i) => {
// 随机生成30-80px的高度
const height = 30 + Math.random() * 50;
return { text: `动态高度列表项 ${i + 1}`, height: Math.floor(height) };
});
// 存储每个列表项的位置信息
const positionInfo = [];
let totalHeight = 0;
data.forEach((item, index) => {
positionInfo.push({
index,
height: item.height,
top: totalHeight,
bottom: totalHeight + item.height
});
totalHeight += item.height;
});
const containerHeight = container.clientHeight;
const phantom = document.createElement('div');
phantom.style.height = `${totalHeight}px`;
phantom.style.position = 'relative';
container.appendChild(phantom);
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.left = '0';
content.style.top = '0';
content.style.right = '0';
phantom.appendChild(content);
// 根据滚动偏移量找到起始索引
function findStartIndex(scrollTop) {
let left = 0;
let right = positionInfo.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (positionInfo[mid].bottom <= scrollTop) {
left = mid + 1;
} else if (positionInfo[mid].top > scrollTop) {
right = mid - 1;
} else {
return mid;
}
}
return left;
}
function renderContent() {
const scrollTop = container.scrollTop;
const startIndex = findStartIndex(scrollTop);
// 计算结束索引
let endIndex = startIndex;
let accumulatedHeight = 0;
while (endIndex < positionInfo.length && accumulatedHeight < containerHeight) {
accumulatedHeight += positionInfo[endIndex].height;
endIndex++;
}
// 多渲染2项避免空白
endIndex = Math.min(endIndex + 2, positionInfo.length);
// 设置偏移量
content.style.transform = `translateY(${positionInfo[startIndex].top}px)`;
// 渲染内容
let html = '';
for (let i = startIndex; i < endIndex; i++) {
const item = data[i];
html += `<div class="list-item" style="height:${item.height}px;line-height:${item.height}px">${item.text}</div>`;
}
content.innerHTML = html;
}
container.addEventListener('scroll', renderContent);
renderContent();
方案三:横向虚拟滚动
适用于横向排列的长列表场景,逻辑和纵向虚拟滚动类似,只是计算维度从高度换成宽度。
实现代码
const container = document.getElementById('container');
const data = Array.from({ length: 10000 }, (_, i) => `横向项 ${i + 1}`);
const itemWidth = 120;
const containerWidth = container.clientWidth;
const visibleCount = Math.ceil(containerWidth / itemWidth);
const totalWidth = data.length * itemWidth;
const phantom = document.createElement('div');
phantom.style.width = `${totalWidth}px`;
phantom.style.height = '100%';
phantom.style.position = 'relative';
phantom.style.display = 'flex';
container.appendChild(phantom);
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.top = '0';
content.style.left = '0';
content.style.bottom = '0';
content.style.display = 'flex';
phantom.appendChild(content);
function renderContent() {
const scrollLeft = container.scrollLeft;
const startIndex = Math.floor(scrollLeft / itemWidth);
const endIndex = Math.min(startIndex + visibleCount + 2, data.length);
content.style.transform = `translateX(${startIndex * itemWidth}px)`;
let html = '';
for (let i = startIndex; i < endIndex; i++) {
html += `<div class="list-item" style="width:${itemWidth}px;flex-shrink:0">${data[i]}</div>`;
}
content.innerHTML = html;
}
container.addEventListener('scroll', renderContent);
renderContent();
方案四:DOM节点复用虚拟滚动
在基础固定高度方案上,复用已有的DOM节点,避免频繁创建和销毁DOM,进一步提升性能。
实现代码
const container = document.getElementById('container');
const data = Array.from({ length: 10000 }, (_, i) => `复用DOM项 ${i + 1}`);
const itemHeight = 50;
const containerHeight = container.clientHeight;
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
const totalHeight = data.length * itemHeight;
const phantom = document.createElement('div');
phantom.style.height = `${totalHeight}px`;
phantom.style.position = 'relative';
container.appendChild(phantom);
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.left = '0';
content.style.top = '0';
content.style.right = '0';
phantom.appendChild(content);
// 预先创建固定数量的DOM节点
const itemNodes = [];
for (let i = 0; i < visibleCount; i++) {
const node = document.createElement('div');
node.className = 'list-item';
node.style.height = `${itemHeight}px`;
node.style.lineHeight = `${itemHeight}px`;
content.appendChild(node);
itemNodes.push(node);
}
let prevStartIndex = -1;
function renderContent() {
const scrollTop = container.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
// 如果起始索引没变,不需要重新渲染
if (startIndex === prevStartIndex) return;
prevStartIndex = startIndex;
const endIndex = Math.min(startIndex + visibleCount, data.length);
content.style.transform = `translateY(${startIndex * itemHeight}px)`;
// 复用DOM节点更新内容
for (let i = 0; i < itemNodes.length; i++) {
const dataIndex = startIndex + i;
if (dataIndex < endIndex) {
itemNodes[i].textContent = data[dataIndex];
itemNodes[i].style.display = 'block';
} else {
itemNodes[i].style.display = 'none';
}
}
}
container.addEventListener('scroll', renderContent);
renderContent();
方案五:结合IntersectionObserver的虚拟滚动
使用IntersectionObserver监听列表项的可见性,替代scroll事件的计算,减少主线程开销。
实现代码
const container = document.getElementById('container');
const data = Array.from({ length: 10000 }, (_, i) => `Observer项 ${i + 1}`);
const itemHeight = 50;
const totalHeight = data.length * itemHeight;
const phantom = document.createElement('div');
phantom.style.height = `${totalHeight}px`;
phantom.style.position = 'relative';
container.appendChild(phantom);
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.left = '0';
content.style.top = '0';
content.style.right = '0';
phantom.appendChild(content);
// 初始渲染第一批数据
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const node = entry.target;
const index = Number(node.dataset.index);
// 更新可见项的内容
node.textContent = data[index];
}
});
}, {
root: container,
threshold: 0.1
});
// 先创建占位节点
for (let i = 0; i < data.length; i++) {
const node = document.createElement('div');
node.className = 'list-item';
node.style.height = `${itemHeight}px`;
node.style.lineHeight = `${itemHeight}px`;
node.dataset.index = i;
content.appendChild(node);
observer.observe(node);
}
// 初始设置偏移量
content.style.transform = `translateY(0px)`;
方案选择建议
| 方案 | 适用场景 | 性能表现 |
|---|---|---|
| 固定高度基础版 | 列表项高度固定 | 最优 |
| 动态高度版 | 列表项高度不固定 | 中等,需要维护位置信息 |
| 横向虚拟滚动 | 横向排列长列表 | 优 |
| DOM复用版 | 需要极致性能的固定高度场景 | 最优 |
| IntersectionObserver版 | 希望减少scroll事件计算开销 | 中等,兼容性需要注意 |
实际开发中可以根据业务场景选择合适的方案,固定高度场景优先选择DOM复用版或者基础版,动态高度场景选择动态高度方案,横向场景选择横向虚拟滚动方案即可。
virtual_scroll前端性能优化长列表渲染JS虚拟滚动DOM复用修改时间:2026-06-22 07:49:04