Back

虚拟滚动:构建高性能界面

虚拟滚动:构建高性能界面

在浏览器中渲染 50 万行数据,你的界面很可能会冻结、卡顿或崩溃。虽然现代浏览器可以处理大型 DOM 树,但随着节点数量的增长,由于布局、样式计算和内存开销,性能往往会急剧下降。虚拟滚动通过仅渲染用户实际能看到的内容来解决这个问题——而这一个约束条件就彻底改变了数据密集型界面的性能表现。

核心要点

  • 虚拟滚动仅渲染视口中可见的项目(加上一个小的缓冲区),无论数据集大小如何,都能保持 DOM 节点数量恒定。
  • 它通过从 scrollTop 计算可见索引、渲染这些项目,并使用填充元素模拟完整的可滚动高度来工作。
  • 固定项目高度使实现保持简单。动态高度需要测量缓存和仔细的滚动位置校正。
  • 浏览器原生搜索(Ctrl+F)、可访问性和滚动位置稳定性在虚拟化列表中都需要额外关注。
  • React、Angular 和 Vue 都有成熟的库——在生产环境中很少需要从头构建。

什么是虚拟滚动(以及它为什么不是无限滚动)?

虚拟滚动(也称为列表虚拟化或窗口化)仅渲染当前在视口中可见的项目,加上上下各一小段缓冲区。当用户滚动时,离开视口的项目会从 DOM 中移除,新项目会在其位置插入。整个数据集永远不会完全进入 DOM。

这与无限滚动有本质区别。无限滚动在你滚动时追加项目到 DOM——列表持续增长。虚拟滚动交换项目的进出,无论数据集大小如何,都能保持 DOM 节点数量大致恒定。

实际差异很显著。一个简单渲染的 10 万项列表可能会在内存中创建 10 万个以上的 DOM 节点。而同样数据集的虚拟化列表在任何给定时刻可能只持有 50-80 个节点。

虚拟化列表的概念原理

该机制依赖于几个简单的想法协同工作:

视口窗口。 你为滚动容器设置一个固定高度。这定义了一次可见多少项目——称为 visibleCount = Math.ceil(containerHeight / itemHeight)

索引计算。 当用户滚动时,你读取 scrollTop 来确定哪个项目位于可见区域的顶部:startIndex = Math.floor(scrollTop / itemHeight)。结束索引随之而来:endIndex = startIndex + visibleCount

滚动位置错觉。 如果你只渲染 50 个项目,滚动条会反映一个很小的列表。为了模拟完整高度,你在渲染项目上方放置一个空的填充元素(高度 = startIndex × itemHeight),下方再放置一个(高度 = 剩余空间)。滚动条的行为就像完整数据集存在一样。

过扫描(缓冲区)。 仅渲染精确可见的项目会在快速滚动时造成突兀的弹出效果。过扫描在视口上下额外渲染几行——通常是 5-10 个项目,取决于使用场景——这样项目在滑入视图之前就已经在 DOM 中了。

固定高度 vs. 动态高度

固定高度虚拟化简单可靠。每个计算都是简单的算术运算。

动态高度要困难得多。你需要在渲染后测量每个项目并缓存这些测量值,或者预先估计高度并在测量后进行校正。这两种方法都增加了复杂性,如果处理不当可能导致滚动位置不稳定。如果你的使用场景允许,固定高度值得设计采用。

需要预期的实际权衡

虚拟滚动不是免费的。有几件事会失效或需要额外工作:

  • 浏览器文本搜索(Ctrl+F) 停止可靠工作,因为大部分内容不在 DOM 中。你需要实现自己的搜索功能。
  • 可访问性需要关注。为容器应用 role="list"role="feed"role="grid"。你可以使用 aria-setsizearia-posinset 等属性,让辅助技术能够理解完整列表大小和每个项目的位置。维护焦点管理,以便在项目卸载时键盘导航不会中断。小的过扫描缓冲区也有助于屏幕阅读器检测到存在更多内容。
  • 滚动位置稳定性在数据动态更新时变得棘手——在当前滚动位置上方添加或删除项目可能导致突兀的跳动。

跨框架的生态系统支持

在生产环境中,你很少需要从头构建这个功能。成熟的库处理了边缘情况:

一个值得了解的 CSS 替代方案:content-visibility: auto 让浏览器在没有 JavaScript 的情况下跳过渲染屏幕外内容。它可以改善中等列表的绘制性能,但不会减少 DOM 节点数量,也不能替代大型数据集的完全虚拟化。

何时真正使用它

虚拟滚动增加了复杂性。在以下情况下值得使用:

  • 你的列表超过几百个项目,并且滚动性能明显下降
  • 你正在构建表格、日志查看器、信息流或电子表格样式的界面
  • 内存使用是一个限制因素(移动设备、长时间运行的会话)

对于短列表,分页或简单的懒加载通常更简单且足够好。

结论

用户不需要 10 万个 DOM 节点——他们需要感觉能够滚动浏览 10 万个项目。虚拟滚动以极小的渲染成本提供了这种感觉。通过仅渲染数据集的可见切片,并在用户滚动时交换项目的进出,你可以保持 DOM 节点数量低、内存使用可预测、帧率流畅。权衡——失效的 Ctrl+F、可访问性考虑、滚动位置管理——是真实存在的但已被充分理解,而且 React、Angular 和 Vue 的库生态系统开箱即用地处理了其中大部分问题。如果你的列表大到足以损害性能,虚拟化是最有效的可用工具。

常见问题

可以,但需要额外小心。大多数虚拟化库假设单列列表布局。对于基于网格的布局,你需要一起计算可见的行和列,考虑每行的项目数。TanStack Virtual 原生支持网格虚拟化。对于其他库,你可能需要将每行视为包含多个单元格的单个虚拟化项目。

搜索引擎爬虫通常不会滚动内容,因此初始渲染之外的项目不会被索引。如果 SEO 对你的列表内容很重要,考虑分页的 HTML 输出或对爬虫友好的替代方案。如果你渲染服务器端内容,仅在客户端水合后应用虚拟化。

这通常意味着你的过扫描缓冲区太小或项目渲染太慢。增加过扫描计数,以便预渲染更多屏幕外项目。还要检查列表项是否触发昂贵的布局重新计算或同步加载图像。简化项目组件并为图像使用占位符内容可以显著减少空白帧。

可以。在导航之前,将当前的 scrollTop 值和相应的 startIndex 保存到状态或会话存储中。当用户返回时,将滚动容器位置恢复到保存的 scrollTop 值。大多数虚拟化库公开了 scrollToIndex 或 scrollToOffset 方法,使得在重新挂载时实现这一点变得简单。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay