导读:本期聚焦于沈清秋创作的《JavaScript如何实现虚拟列表?掌握滚动性能优化的核心思路》,敬请观看详情。当页面需要渲染上万条数据时,DOM节点数量暴涨会导致滚动卡顿、内存占用飙升。虚拟列表通过只渲染可视区域内的元素,把实际DOM数量控制在几十个以内,从根本上解决了长列表的性能瓶颈。本文将从原生JavaScript实现入手,一步步讲解可视区域计算、起始索引推导、缓冲区设计、滚动偏移补偿等关键步骤,并给出完整可运行的代码示例。同时会对比translate与absolute定位两种方案的区别,分析定高与不定高列表的实现难度差异,最后介绍react-window等成熟库的适用场景,帮助你根据业务需求选择最合适的方案。

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

JavaScript如何实现虚拟列表?掌握滚动性能优化的核心思路

虚拟列表的核心原理:只渲染看得见的部分

虚拟列表的思路说白了很简单:假设列表有一万条数据,但屏幕高度只有800像素,每条数据占50像素,那么用户任意时刻最多只能看到16条左右。既然看不见的 DOM 没有任何意义,那就只把这16条挂到页面上,其余的用空白撑起总高度,让滚动条看起来像完整列表一样。

具体拆解一下,实现需要三个角色协同工作。第一个是滚动容器,它有固定高度,设置overflow-y: auto,负责产生滚动事件。第二个是占位元素,也叫幽灵层,它的高度等于数据总高度(数据条数乘以单项高度),作用是让滚动条长度正确,否则浏览器根本不知道该把滚动条拉多长。第三个是渲染区域,通常是绝对定位的容器,里面放真正可见的那批列表项,通过transformtop属性把它挪到正确的位置。

滚动时要做的事情只有三步:读取容器的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挂载后,通过getBoundingClientRectResizeObserver拿到每项的实际高度,缓存到一个位置数组里,下次计算索引时基于缓存值查找。这种方案的难点在于需要维护一个“位置映射表”,并且当某项高度变化时要更新它后面所有项的起始位置,通常用前缀和数组加二分查找来加速定位。查找过程如下:

// 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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52662.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。