列表渲染是前端页面里最常见的需求之一,但一旦数据量上升到几千条甚至上万条,直接把所有数据都渲染成DOM的方案就会彻底崩掉:页面首次渲染耗时数秒,滚动时掉帧严重,内存占用居高不下。虚拟列表(Virtual List)就是针对这个问题的经典解法——不渲染全部数据,只渲染用户当前看得见的一小部分。这篇文章用原生JavaScript把虚拟列表从零实现一遍,把里面容易踩坑的细节讲清楚。

虚拟列表的核心原理:只渲染看得见的部分
虚拟列表的思路说白了很简单:假设列表有一万条数据,但屏幕高度只有800像素,每条数据占50像素,那么用户任意时刻最多只能看到16条左右。既然看不见的 DOM 没有任何意义,那就只把这16条挂到页面上,其余的用空白撑起总高度,让滚动条看起来像完整列表一样。
具体拆解一下,实现需要三个角色协同工作。第一个是滚动容器,它有固定高度,设置overflow-y: auto,负责产生滚动事件。第二个是占位元素,也叫幽灵层,它的高度等于数据总高度(数据条数乘以单项高度),作用是让滚动条长度正确,否则浏览器根本不知道该把滚动条拉多长。第三个是渲染区域,通常是绝对定位的容器,里面放真正可见的那批列表项,通过transform或top属性把它挪到正确的位置。
滚动时要做的事情只有三步:读取容器的scrollTop,用公式Math.floor(scrollTop / itemHeight)算出当前第一条可见数据的索引;从数据数组里截取从该索引开始、数量为Math.ceil(containerHeight / itemHeight) + 1的一段;最后更新渲染区域的内容和偏移量。整个过程的DOM数量恒定,无论数据是一万条还是一百万条,渲染开销都不变。
用原生JavaScript实现一个定高虚拟列表
先从最简单的定高场景入手,也就是每个列表项高度固定。这是虚拟列表的基础形态,理解了它,后面的变体都只是在这个骨架上做修补。下面是完整的HTML结构:
<div id="scroll-container" style="height: 600px; overflow-y: auto; position: relative;"> <div id="placeholder"></div> <div id="render-list" style="position: absolute; top: 0; left: 0; right: 0;"></div> </div>
注意渲染区域用了绝对定位,并且top: 0,后面靠transform来移动它,这样做比直接改top性能更好,因为transform不触发重排,只触发合成。接着是JavaScript部分:
const container = document.getElementById('scroll-container');
const placeholder = document.getElementById('placeholder');
const renderList = document.getElementById('render-list');
const ITEM_HEIGHT = 50; // 每项固定高度
const BUFFER_COUNT = 5; // 上下缓冲条数
const VISIBLE_COUNT = Math.ceil(600 / ITEM_HEIGHT) + 1; // 13
// 模拟一万条数据
const data = Array.from({ length: 10000 }, (_, i) => `第 ${i + 1} 条数据`);
placeholder.style.height = data.length * ITEM_HEIGHT + 'px';
function render(scrollTop) {
// 计算起始索引,减去缓冲,注意边界
let start = Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER_COUNT;
start = Math.max(0, start);
const end = Math.min(data.length, start + VISIBLE_COUNT + BUFFER_COUNT * 2);
// 只生成可见片段的DOM
renderList.innerHTML = data.slice(start, end)
.map(text => `<div style="height:${ITEM_HEIGHT}px; line-height:${ITEM_HEIGHT}px; border-bottom:1px solid #eee;">${text}</div>`)
.join('');
// 关键一步:把渲染区域整体下移到正确位置
renderList.style.transform = `translateY(${start * ITEM_HEIGHT}px)`;
}
container.addEventListener('scroll', () => {
// 用 requestAnimationFrame 保证一帧只渲染一次
requestAnimationFrame(() => render(container.scrollTop));
});
render(0);这段代码里有几个细节值得展开。首先是缓冲区(BUFFER_COUNT)的作用:如果只在精确的可见范围内渲染,快速滚动时会出现白屏,因为新内容还没来得及挂载。上下各多渲染几条,能显著缓解这个问题。其次是偏移补偿:起始索引乘以单项高度得到translateY的值,这一步不能漏,否则渲染出来的内容会堆在列表顶部。最后是requestAnimationFrame的节流,scroll事件在部分设备上一秒能触发上百次,直接在回调里重绘会浪费大量计算,用rAF合并到每帧执行一次是最经济的做法。
不定高列表与滚动补偿的处理
定高方案虽然简单,但真实业务里列表项往往高度不一:一段长文案、一张图片、一个展开的评论区,都会让itemHeight失效。不定高列表是虚拟列表实现的分水岭,处理思路主要有两种。
第一种是预估高度加实际测量。初始渲染时用一个估计值(比如80像素)计算占位总高度,等真实DOM挂载后,通过getBoundingClientRect或ResizeObserver拿到每项的实际高度,缓存到一个位置数组里,下次计算索引时基于缓存值查找。这种方案的难点在于需要维护一个“位置映射表”,并且当某项高度变化时要更新它后面所有项的起始位置,通常用前缀和数组加二分查找来加速定位。查找过程如下:
// positions 记录每项的起始偏移,prefix sum 结构
// positions[i] = positions[i-1] + measuredHeights[i-1]
function findStartIndex(positions, scrollTop) {
let low = 0, high = positions.length - 1;
while (low < high) {
const mid = (low + high) >> 1;
if (positions[mid] < scrollTop) {
low = mid + 1;
} else {
high = mid;
}
}
return Math.max(0, low - 1); // 回退一项,保证覆盖
}第二种是让内容自身撑开高度,不依赖测量。适用于粗略场景:把渲染区域从绝对定位改为普通文档流,放在一个大的padding容器里,用padding-top等于起始项之前所有项的总高度来顶下去。这种方式实现简单,不需要位置数组,但滚动时高度抖动比较明显,用户体验一般,适合对精度要求不高的后台管理页面。
另外提一个不定高场景特有的坑:滚动到某一项。定高时scrollTo(index * itemHeight)一句话搞定,不定高时如果目标项还没被测量过,只能用预估高度算位置,滚动过去后实际位置有偏差,需要二次修正。成熟的库一般会在滚动完成后再校验一次目标项是否对齐,没对齐就补一次滚动。
进阶优化与现有库的选型建议
自己实现虚拟列表适合理解原理和小规模项目,如果是生产环境,更推荐直接用成熟库。react-window是React生态里最轻量的选择,核心代码不到6KB,API简洁,支持定高和不定高两种模式,作者是Jason Miller这样的性能优化专家;react-virtualized功能更全(表格、瀑布流、无限加载都有),但体积和复杂度也更高;Vue生态对应的有vue-virtual-scroller。选型时可以参考下表:
| 方案 | 体积 | 不定高支持 | 适用场景 |
|---|---|---|---|
| 手写实现 | 0 | 需自行实现 | 学习原理、无依赖要求的简单列表 |
| react-window | 约6KB | 支持(DynamicSizeList) | 绝大多数React项目的首选 |
| react-virtualized | 约30KB | 支持 | 需要表格、多网格等复杂布局 |
除了选库,还有几个通用优化点值得注意。第一,列表项组件要避免在渲染函数里做昂贵计算,虚拟列表滚动时组件会频繁挂载卸载,任何重复开销都会被放大;第二,给列表项加key时务必使用稳定的业务ID而不是数组下标,否则复用会错乱;第三,如果数据还会动态变化(插入、删除),记得同步更新占位高度和位置缓存,否则滚动条长度会和内容对不上。
总结一下,虚拟列表的本质是用恒定的DOM数量换取对任意规模数据的支持:定高场景几十行代码就能落地,不定高场景需要引入位置映射和测量机制。先读懂原理再决定手写还是用库,才能在性能和可维护性之间找到平衡点。
虚拟列表滚动性能优化JavaScript修改时间:2026-09-08 07:20:56