纯前端实现虚拟滚动:高度缓存、锚点保持与滚动抖动治理

举报
yd_213843956 发表于 2026/10/24 07:46:08 2026/10/24
【摘要】 列表一长,DOM 数量就成了性能瓶颈。这篇文章讲清楚虚拟滚动在可变高度场景下的三个关键问题:怎么量高度、怎么定位起点、怎么在插入删除时保持视口不跳。

虚拟滚动的基本思路不复杂:只渲染视口附近的若干条,其余用上下两个占位块撑起滚动高度。真正难的是列表项高度不固定的时候——高度怎么量、起点怎么找、数据变了怎么不跳。这篇文章按这三个问题展开。

一、先量高度,再谈渲染

固定高度只需要乘法,可变高度就必须知道每一项的实际高度。最稳妥的方式是渲染后测量,并用 ResizeObserver 持续更新:

const heights = new Map();
const ro = new ResizeObserver((entries) => {
  for (const e of entries) {
    const id = e.target.dataset.id;
    const h = e.target.getBoundingClientRect().height;
    if (heights.get(id) !== h) {
      heights.set(id, h);
      scheduleUpdate();
    }
  }
});

没有量过的项先用一个估算值占位,等真实高度回来再修正。注意不要在每次更新时清空缓存,否则列表滚动到中间时会反复跳变。

二、前缀和 + 二分,快速定位起点

知道每项高度后,把所有高度累加成前缀和数组。给定滚动距离,用二分找到第一个累计高度大于它的下标,就是当前视口的起始项:

function findStart(prefix, scrollTop) {
  let lo = 0, hi = prefix.length - 1;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (prefix[mid] < scrollTop) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}

前缀和要在高度变化时增量更新。如果列表很长,逐项累加会很慢,可以按块(比如每 100 项)维护块内高度和,定位时先找块再找项,把更新成本摊薄。

三、锚点保持:数据变化时视口别跳

虚拟滚动最容易被忽略的场景是「列表上方插入或删除了数据」。这时所有下标都会平移,如果还按原来的 scrollTop 渲染,用户眼前的内容就会突然换掉。

做法是记录「锚点」:当前视口第一项的 id 和它距离视口顶部的偏移。数据更新后,重新计算这个 id 的新位置,再把滚动位置调到「新位置 − 偏移」:

const offset = scrollTop - prefix[anchorIndex];
const newIndex = indexOfId(anchorId);
scrollTop = prefix[newIndex] + offset;

这样无论上方插入多少条,用户正在看的那一项都会停在原来的位置,体验上就像什么都没发生。

四、滚动抖动:一帧只更新一次

滚动事件触发频率远高于屏幕刷新率,直接在监听里改 DOM 很容易掉帧。标准做法是用 requestAnimationFrame 合并同一帧里的多次事件:

let ticking = false;
function onScroll() {
  if (ticking) return;
  ticking = true;
  requestAnimationFrame(() => {
    ticking = false;
    render(window.scrollY);
  });
}

另一个细节是上下占位块的高度。给占位块设置高度会触发重排,频繁修改时可以用 transform 撑开,或者把占位块放在独立图层里,减少对其他内容的影响。

五、几个容易踩的坑

图片加载后高度变化。没有写死宽高比的图片,加载完成会把后面的项整体推下去。给图片容器设置 aspect-ratio,或者预先占位,能避免大部分跳动。

快速拖动滚动条。一帧内跨过几百项时,用二分定位依然很快,但不要把中间项都渲染出来,只渲染最终视口附近的部分即可。

展开/收起。折叠面板展开会改变高度,必须重新测量并触发前缀和更新,否则下面的滚动位置会错位。

键盘导航。用上下键移动焦点时,如果目标项不在渲染范围内,需要先滚动到它,等它渲染出来再 focus,不能直接对未挂载的节点调用 focus。

六、小结

虚拟滚动真正的复杂度在「高度不确定」和「数据会变」这两件事上。把测量、前缀和、锚点保持拆成三个独立模块,每个模块都能单独测试;渲染层只负责根据计算结果输出可见项,逻辑会清楚很多。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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