列表一滚就卡:先控制页面里的DOM数量

举报
yd_232671198 发表于 2026/11/01 00:05:24 2026/11/01
【摘要】 一个后台页面要展示几万条记录,接口只用了几百毫秒,页面却在渲染时明显停顿。继续滚动,还会出现掉帧。此时如果只盯着接口速度,往往找不到真正的问题。前端需要为每一行创建元素、计算样式、完成布局,并处理后续更新。即使每行只有几个标签,几万行累积起来也会形成很大的页面结构。先在性能工具里区分网络等待、JavaScript 执行和布局绘制的时间,比直接换框架更有帮助。如果用户主要逐页查看记录,分页通常...

一个后台页面要展示几万条记录,接口只用了几百毫秒,页面却在渲染时明显停顿。继续滚动,还会出现掉帧。此时如果只盯着接口速度,往往找不到真正的问题。

前端需要为每一行创建元素、计算样式、完成布局,并处理后续更新。即使每行只有几个标签,几万行累积起来也会形成很大的页面结构。先在性能工具里区分网络等待、JavaScript 执行和布局绘制的时间,比直接换框架更有帮助。

如果用户主要逐页查看记录,分页通常是最直接的方案。产品确实需要连续滚动时,可以考虑虚拟列表:保留完整的滚动空间,只创建当前可见范围附近的行。视口外的内容暂时不进入页面,需要时再替换进来。

下面是一个固定行高的简化示例,可以保存为 HTML 文件观察效果。它创建一万条数据,但每次只渲染视口附近的一小段。

<div id="viewport" style="height:320px;overflow:auto">
  <div id="spacer" style="position:relative">
    <div id="rows" style="position:absolute;inset:0 0 auto"></div>
  </div>
</div>
<script>
const viewport = document.querySelector('#viewport');
const spacer = document.querySelector('#spacer');
const rows = document.querySelector('#rows');
const data = Array.from({ length: 10000 }, (_, i) => `记录 ${i + 1}`);
const rowHeight = 36;
const overscan = 5;
spacer.style.height = `${data.length * rowHeight}px`;

function render() {
  const start = Math.max(0,
    Math.floor(viewport.scrollTop / rowHeight) - overscan);
  const end = Math.min(data.length,
    Math.ceil((viewport.scrollTop + viewport.clientHeight) / rowHeight)
      + overscan);
  rows.style.transform = `translateY(${start * rowHeight}px)`;
  const nodes = data.slice(start, end).map(text => {
    const row = document.createElement('div');
    row.style.cssText = `height:${rowHeight}px;box-sizing:border-box;`
      + 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
    row.textContent = text;
    return row;
  });
  rows.replaceChildren(...nodes);
}

let scheduled = false;
viewport.addEventListener('scroll', () => {
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    scheduled = false;
    render();
  });
}, { passive: true });
render();
</script>

外层容器负责滚动,中间的占位层提供全部数据对应的高度。内层只承载可见行,并通过位移放到正确位置。额外渲染的五行属于预留区域,可以降低快速滚动时边缘露白的概率。这里用一帧一次的调度合并滚动事件,避免同一帧重复重建内容。

这个示例的前提很明确:每行实际占用的高度始终是三十六像素。边框、内边距、文本换行和图片加载都可能破坏这个前提,因此示例设置了盒模型并限制换行。动态行高需要测量高度、维护累计位置,并在测量变化后调整滚动偏移,复杂度会明显增加。正式项目如果已有成熟组件,优先使用它提供的虚拟滚动能力。

虚拟列表减少的是页面元素数量,数据处理成本仍然存在。如果每次滚动都对全部记录排序、搜索或者执行复杂计算,页面依然会卡。排序和筛选应放在数据变化时处理;可见区渲染则尽量只做当前区间需要的工作。示例每次替换行是为了展示原理,实际组件通常还会复用节点并跳过未变化的区间。

还需要关注交互状态。行离开视口后可能被卸载,输入值、展开状态和选中状态应由稳定的数据标识关联,不能只依赖某个临时节点。键盘焦点、屏幕阅读器、浏览器页内查找也可能受到影响,应根据产品需求选择合适的组件和替代操作方式。

验证优化时,不必先追求一个漂亮的跑分。可以检查滚动到列表中段后,页面中的行节点是否仍然保持在一个较小范围;再用性能录制比较滚动期间的长任务和布局耗时。最后测试快速拖动滚动条、窗口缩放和跳转到末尾,确认没有空白或错位。

数据量大并不意味着必须一次渲染全部数据。先确定用户真正需要看见多少内容,再让页面承担相应的工作量,往往比在每个单元格上零散地加缓存更有效。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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