前端性能优化-长列表虚拟滚动
在前端开发中,一次性渲染大批量数据的列表是性能杀手。一次性创建数万个 DOM 节点,导致浏览器样式计算和布局耗时巨大,会造成首屏加载白屏、滚动严重掉帧。可以使用虚拟滚动 进行优化。
1.核心思想:只渲染用户“看得见”的那部分 DOM 元素
想象一个滚动的长条,虽然数据有 10000 条,但用户的屏幕(视口)只能看到 10 条。我们只需要创建这 10 个节点的 DOM,随着滚动动态更新它们的内容和偏移量。
基本组成部分:
- 外部容器(Container): 固定高度。
- 撑高元素(Phantom): 一个不可见的元素,高度等于
总数据量 * 每项高度。它的作用是让滚动条显示出正确的高度。 - 渲染列表(Visible List): 绝对定位在容器内,内容随滚动实时计算。
2. 设置变量
1.总数据量: DataSize
2.每项高度: ItemHeight
3.容器高度: ContainerHeight
4.初始化撑高元素高度: PhantomHeight = DataSize * ItemHeight
5.可视区域显示数量:
6.当前滚动的起始索引:
7.列表的偏移量:
3. 实现步骤
第一步:设置容器
外层容器设为 relative 定位,内部“撑高元素”高度设为 total * itemHeight。
第二步:监听滚动
监听容器的 onScroll 事件,实时获取 scrollTop。
第三步:更新数据切片
根据 scrollTop 计算出当前应该显示的 startIndex 和 endIndex,然后从原始数组中 slice 出这一段数据进行渲染。
第四步:调整偏移
因为滚动条在往下走,为了不让渲染的列表被“卷上去”,需要给列表容器设置一个 transform: translateY(${offset}px),手动将其拉回视口。
简易版虚拟滚动:
<div id="container" style="height: 400px; overflow-y: auto; position: relative; border: 1px solid #ccc;">
<div id="phantom" style="position: absolute; left: 0; top: 0; right: 0; z-index: -1;"></div>
<div id="list" style="position: absolute; left: 0; top: 0; right: 0;"></div>
</div>
<script>
// 长列表
const listData = Array(50000).fill().map((_, i) => `项目${i}`);
const container = document.getElementById('container');
const phantom = document.getElementById('phantom');
const list = document.getElementById('list');
const DATA_SIZE = listData.length; // 总数据量
const ITEM_HEIGHT = 50; // 每行固定高度
const VIEW_HEIGHT = 400; // 容器高度
const VISIBLE_COUNT = Math.ceil(VIEW_HEIGHT / ITEM_HEIGHT); // 可视数量
// 初始化撑高元素
phantom.style.height = DATA_SIZE * ITEM_HEIGHT + 'px';
function update () {
const scrollTop = container.scrollTop;
// 1. 计算起始索引
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
const endIndex = startIndex + VISIBLE_COUNT;
// 2. 截取数据并渲染
const items = [];
for (let i = startIndex; i < endIndex && i < DATA_SIZE; i++) {
items.push(`<div style="height:${ITEM_HEIGHT}px; border-bottom:1px solid #eee;">${listData[i]}</div>`);
}
list.innerHTML = items.join('');
// 3. 偏移列表,使其保持在视口内 (核心:偏移量 = 滚动距离 - 滚动距离 % 每项高度)
const offset = scrollTop - (scrollTop % ITEM_HEIGHT);
list.style.transform = `translate3d(0, ${offset}px, 0)`;
}
container.addEventListener('scroll', update);
update(); // 初始加载
</script>
4.优化点
在实际工程中,可进行如下优化:
- 缓冲区(Buffer): 在可视区上下额外多渲染 2-3 个元素,防止用户快速滑动时出现白屏。
const BUFFER_SIZE = 5; // 上下各额外渲染 5 个
function updateAdvanced () {
const scrollTop = container.scrollTop;
// 计算原始索引
let startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
let endIndex = startIndex + VISIBLE_COUNT;
// 应用缓冲区
const renderStart = Math.max(0, startIndex - BUFFER_SIZE);
const renderEnd = Math.min(DATA_SIZE, endIndex + BUFFER_SIZE);
console.log('渲染范围:', renderStart, renderEnd);
console.log('实际范围:', startIndex, endIndex);
// 渲染逻辑
const items = [];
for (let i = renderStart; i < renderEnd; i++) {
items.push(`<div style="height:${ITEM_HEIGHT}px;">${listData[i]}</div>`);
}
list.innerHTML = items.join('');
// 此时偏移量的计算需要基于 renderStart 所在的像素位置
const offset = renderStart * ITEM_HEIGHT;
list.style.transform = `translate3d(0, ${offset}px, 0)`;
}
- 不定高度处理:
- 给每个 Item 一个预估高度。
- 在渲染后(
updated生命周期)获取 DOM 的真实高度。 - 维护一个位置缓存表(Position Cache),记录每一项的
top和bottom。
5. 性能对比
加载长度为50000的长列表,通过 Chrome DevTools 的实测:
直接渲染:

使用虚拟滚动:
