首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >移动端 H5 长列表不卡顿:虚拟滚动的原理与工程落地

移动端 H5 长列表不卡顿:虚拟滚动的原理与工程落地

原创
作者头像
用户5598620
发布2026-09-12 09:07:02
发布2026-09-12 09:07:02
470
举报

移动端 H5 里最常见的卡顿场景,就是长列表:订单记录、聊天消息、商品列表、数据明细,动辄上千条。直接把所有节点渲染进 DOM,Android 低端机上列表滑动会明显掉帧,iOS 上内存也会被大量离屏节点吃满。虚拟滚动(窗口化渲染)的思路是:只渲染可视区域附近的一小批节点,其余用空白占位,滚动时按需重建。本文讲清楚它为什么有效、实现时有哪些必须处理的坑,以及一套可直接抄走的移动端方案。

一、为什么"全量渲染"在移动端会卡

一个 1000 行的列表,如果每个 li 里还有图片、按钮、多层嵌套,DOM 节点轻松过万。两个成本随节点数线性上涨:

  1. 布局与绘制的成本:每次滚动触发重排时,浏览器都要重新计算所有可见节点的位置。节点越多,单帧内能完成的工作越少,超过 16.7ms 就开始掉帧。
  2. 内存成本:移动端浏览器对 DOM 内存敏感,节点过多会导致页面变慢、甚至 iOS Safari 直接白屏或被杀进程。

虚拟滚动把"渲染全部"改成"渲染视口内的 20~40 条",理论上滚动性能与数据总量无关。

二、虚拟滚动的核心模型:三段式

代码语言:javascript
复制
// 1. 数据总量与行高
const TOTAL = 5000;        // 总条数
const ROW_H = 64;          // 固定行高(px)
// 2. 视口信息
const viewportH = window.innerHeight;
const overscan = 6;        // 上下额外多渲染的行数(缓冲)
// 3. 计算渲染区间
function getRange(scrollTop) {
  const start = Math.max(0, Math.floor(scrollTop / ROW_H) - overscan);
  const end = Math.min(TOTAL - 1, Math.ceil((scrollTop + viewportH) / ROW_H) + overscan);
  return [start, end];
}

外层容器高度固定、overflow 滚动;内层用一个大高度 div 撑起总滚动高度;中间只放 [start, end] 区间的行。滚动时重算区间、替换行内容。

三、固定行高是最省事的起点

固定行高(每一行都是同样高度)时,实现最简单:滚动位置除以行高就能算出起始索引,性能也最好。移动端大部分列表(订单、消息、记录)都能用固定行高。

代码语言:javascript
复制
// 简易固定行高虚拟列表
class VirtualList {
  constructor(container, { total, rowH, render }) {
    this.c = container;
    this.total = total;
    this.rowH = rowH;
    this.render = render;
    this.spacer = document.createElement('div');
    this.spacer.style.height = total * rowH + 'px';
    this.holder = document.createElement('div');
    this.c.appendChild(this.spacer);
    this.c.appendChild(this.holder);
    this.c.addEventListener('scroll', () => this.update(), { passive: true });
    this.update();
  }
  update() {
    const [start, end] = getRange(this.c.scrollTop, this.rowH, this.c.clientHeight, this.total);
    this.holder.style.transform = `translateY(${start * this.rowH}px)`;
    this.holder.innerHTML = '';
    for (let i = start; i <= end; i++) this.holder.appendChild(this.render(i));
  }
}

关键点:holder 用 transform: translateY 定位,而不是 top,避免触发重排。

四、不定高列表:预估行高 + 修正

真实业务里很多列表高度不固定(标题折行、内容长短不一)。做法是"先给一个预估高度撑住滚动条,渲染后实测高度再修正":

代码语言:javascript
复制
// 位置表:记录每行的预估/实测高度
positions = []; // {top, height, index}
function estimate(i) {
  positions[i] = positions[i] || { top: i * estH, height: estH };
  return positions[i];
}
// 行渲染完成后回填真实高度,修正后续所有行位置
function correct(i, realH) {
  const pos = positions[i];
  const diff = realH - pos.height;
  if (diff === 0) return;
  pos.height = realH;
  for (let j = i + 1; j < positions.length; j++) positions[j].top += diff;
}

不定高时滚动到中后段容易出现"白屏闪烁",因为预估高度和实际累计高度有偏差,渲染区间被算错。修正表可以缓解,但要注意:每次修正都要从当前行往后重算偏移,数据量大时累积成本高,建议配合二分查找定位起始行。

五、移动端必须处理的 5 个坑

  1. iOS 滚动惯性scroll 事件在 iOS 上频率低且不连续,一定要用 requestAnimationFrame 节流更新,否则滚动停住后列表才慢慢补齐。
  2. 图片懒加载:虚拟列表里离屏行会被销毁,图片必须配合懒加载,否则每次回滚都重新请求。用 loading="lazy" 或 IntersectionObserver 都行,避免把图片 URL 直接塞给离屏行。
  3. 滚动条跳动:不定高列表在修正高度时滚动条会跳,解决思路是把"滚动位置锚定"在用户当前看到的某一行上,而不是锚定在容器顶部。
  4. 事件委托:行节点频繁重建,不要把事件绑在行上,绑定在容器上做事件委托,用 data-index 定位。
  5. 输入框失焦:列表里若有 input,重建行会丢焦点和输入内容。要么在重建前保存值,要么用 contenteditable 或把输入行排除在回收范围外。

六、踩坑清单

现象

解法

未用 rAF 节流

iOS 上滚动后列表补得慢

scroll 回调里 requestAnimationFrame

固定行高套用变高内容

列表上下错位

换不定高方案+位置表修正

图片直连离屏行

反复请求、流量暴增

loading="lazy" / IntersectionObserver

事件绑在行上

行重建后点击失效

容器事件委托 + data-index

列表里有 input

滚动后输入内容丢失

重建前保存 value 或排除该行

七、工程落地建议

选择虚拟滚动方案时,先用数据画像决定行高模型:90% 以上行高等高就上固定行高,简单且性能最好;只有内容长度差异大的才上预估+修正。组件化封装时把"数据源、行渲染函数、滚动容器"三件事解耦,方便复用在订单、消息、日志等多个页面。上生产前用低端 Android 真机做一次长列表滚动实测,关注帧率和内存曲线。如果团队不想从零造轮子,也可以直接采用社区成熟的虚拟列表库,但要确认它支持移动端触摸滚动和 iOS 惯性,而不是只在桌面端表现良好。

八、复盘清单

  • 滚动是否用 rAF 节流,滑动过程是否平滑无白屏
  • 是否用固定行高,若不是是否做了位置修正
  • 离屏行图片是否懒加载,内存曲线是否平稳
  • 事件是否委托到容器,列表内输入框是否有保护
  • 是否在低端 Android 与 iOS 各跑过一遍真机验证

结语

虚拟滚动解决的是"数据量大"和"DOM 数量多"的根本矛盾,是移动端长列表性能优化的标准答案。它的难点不在原理,而在滚动节流、行高修正、图片懒加载这些工程细节。希望这份落地清单能帮你少踩几次坑,一次做对。

以上为通用前端技术实践分享,具体功能以各平台官方实时文档为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么"全量渲染"在移动端会卡
  • 二、虚拟滚动的核心模型:三段式
  • 三、固定行高是最省事的起点
  • 四、不定高列表:预估行高 + 修正
  • 五、移动端必须处理的 5 个坑
  • 六、踩坑清单
  • 七、工程落地建议
  • 八、复盘清单
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档