H5 虚拟列表与长列表渲染

举报
shenlan9755 发表于 2026/10/12 09:08:42 2026/10/12
【摘要】 H5 虚拟列表与长列表渲染列表是页面中最常见的渲染形态。当数据量达到几千条甚至上万条时,一次性渲染全部 DOM 节点会让页面卡顿、内存膨胀,甚至被浏览器强制回收。虚拟列表通过只渲染可视区域内的部分节点,让长列表也能保持流畅。 一、为什么浏览器会卡每一个 DOM 节点都会占用内存,并且节点过多会让样式计算、布局和绘制成本线性上升。即使节点本身简单,几千个节点叠加后,一次滚动仍可能引发上百毫秒...

H5 虚拟列表与长列表渲染

列表是页面中最常见的渲染形态。当数据量达到几千条甚至上万条时,一次性渲染全部 DOM 节点会让页面卡顿、内存膨胀,甚至被浏览器强制回收。虚拟列表通过只渲染可视区域内的部分节点,让长列表也能保持流畅。

一、为什么浏览器会卡

每一个 DOM 节点都会占用内存,并且节点过多会让样式计算、布局和绘制成本线性上升。即使节点本身简单,几千个节点叠加后,一次滚动仍可能引发上百毫秒的长任务。

const itemHeight = 48;
const containerHeight = 600;
const itemCount = 10000;

在这个例子中,如果全部渲染,需要接近五十万个像素高度的容器,其中绝大部分节点用户根本看不到。

二、虚拟列表的基本原理

虚拟列表只渲染可视区域附近的少量节点。组件根据滚动偏移量计算当前应该渲染的起始索引和结束索引,再通过一个与完整列表等高的占位容器维持滚动条比例。

function getVisibleRange(
  scrollTop: number,
  viewportHeight: number,
  itemHeight: number,
  itemCount: number
): { startIndex: number; endIndex: number } {
  const startIndex = Math.floor(scrollTop / itemHeight);
  const visibleCount = Math.ceil(viewportHeight / itemHeight);
  const endIndex = Math.min(
    itemCount - 1,
    startIndex + visibleCount
  );

  return { startIndex, endIndex };
}

itemHeight 是关键参数。如果每个条目高度固定且已知,虚拟列表的实现最简单可靠。

三、等高与动态高度

等高列表假设每个条目的渲染高度一致。但在实际业务中,标题可能一行或两行,文本长度不确定,条目高度并不一致。

动态高度虚拟列表需要在渲染前预估高度,并在真实渲染后测量并缓存结果:

interface ItemMeasure {
  offsetTop: number;
  height: number;
  measured: boolean;
}

条目真实高度发生变化后,要更新后续所有条目的偏移量。这个计算本身可能成为新瓶颈,如果条目差异不大,可以用固定高度加上预估缓冲,避免过度复杂的测量。

四、滚动事件的节流与帧调度

滚动事件在浏览器中会高频触发。处理虚拟列表时,应避免在每次事件中做复杂计算和同步读取布局信息,并把 DOM 更新集中到下一帧。

let frameId = 0;

function handleScroll(scrollTop: number): void {
  if (frameId > 0) return;
  frameId = requestAnimationFrame(() => {
    frameId = 0;
    updateVisibleItems(scrollTop);
  });
}

一帧内多次滚动只触发一次重算,避免重复操作 DOM。需要读取布局信息时,尽量集中在一次批量读取,避免读写交替。

五、上下缓冲区

如果只渲染可视区域内的条目,快速滚动时会先出现空白,再渲染新内容。可以在可视区域上下额外多渲染几个条目作为缓冲。

const overscan = 6;

const bufferedStart = Math.max(0, startIndex - overscan);
const bufferedEnd = Math.min(itemCount - 1, endIndex + overscan);

缓冲区越大,滚动越不容易看到空白,但同时也会增加渲染节点数量。缓冲区大小应根据设备性能和真实滚动体验调整,通常在 4 到 10 之间。

六、占位容器与滚动条

虚拟列表需要一个占位元素撑起与完整列表相同的高度,否则浏览器无法计算正确的滚动条比例。最常见做法是使用一个外层容器保存滚动,内部再放一个总高度的占位元素和绝对定位的渲染层。

.virtual-list {
  position: relative;
  overflow-y: auto;
  height: 600px;
}

.virtual-list-phantom {
  height: 480000px;
}

.virtual-list-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

渲染层内部条目使用绝对定位或 transform 摆放在正确位置,避免依赖文档流反复触发回流。

七、索引定位与快速跳转

如果业务需要“回到顶部”或“跳转到某个分组”,直接设置 scrollTop 即可,无需模拟滚动。

function scrollToIndex(index: number, itemHeight: number): void {
  container.scrollTop = index * itemHeight;
}

动态高度场景下,索引定位要使用缓存的偏移量。如果目标条目尚未测量过,先根据预估高度估算位置,等真实渲染完成后再修正。

八、图片与子组件的更新

可视区域外的条目被卸载后,组件状态和图片请求也随之取消。图片组件应具备取消加载的能力,否则用户快速滚动时仍会发出大量请求。

function loadImage(element: HTMLImageElement): void {
  element.src = element.dataset.src || '';
}

如果列表项包含复杂子组件,应尽量让子组件的状态跟随数据源,而不是保存在组件内部。卸载重建后,业务状态不能丢失。

九、分组与吸附标题

通讯录和商品列表常用分组标题。分组标题的高度与普通条目不同,虚拟列表需要为不同类型的条目分别维护高度或偏移量。

type ListItem =
  | { type: 'group'; title: string }
  | { type: 'item'; id: string; name: string };

如果产品要求标题吸附在顶部,吸附逻辑应在滚动计算中统一处理,而不是让每个标题各自监听滚动事件。

十、什么时候不需要虚拟列表

虚拟列表会增加复杂度,不是所有长数据都必须使用。如果条目只有几十条,普通渲染通常已经足够流畅。可以先通过真实数据测量滚动帧率和内存占用,确认问题后再引入虚拟化。

常见适合虚拟列表的场景:

  • 聊天记录、日志查看器。
  • 商品列表、订单列表。
  • 通讯录和城市选择。
  • 树形结构展开后的扁平列表。

总结

虚拟列表的本质是把渲染范围收敛到用户可见的一小段内容,同时用占位容器保持滚动语义。等高场景优先用固定高度实现,动态高度要控制测量频率并缓存结果。滚动更新集中到帧回调,渲染范围加上下缓冲区,图片和子组件要支持卸载与恢复。先确认性能问题再引入虚拟化,避免为了技术而技术。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。