4、虚拟滚动技术:处理万级数据点的列表与表格

做数据可视化大屏,最怕什么?

我最怕产品经理轻飘飘来一句:「这个表格也就几万行数据,你优化一下呗。」

几万行?浏览器直接给你卡成PPT。你想想看,一个页面里塞了上万条DOM节点,光是渲染就要几百毫秒,滚动起来更是噩梦。我早期有个项目,客户现场演示时,表格一滚动,整个大屏直接白屏了——嗯,那场面,尴尬到脚趾抠地。

后来我学乖了。遇到这种场景,直接上虚拟滚动。

4.1 虚拟滚动到底是个啥?

说白了,虚拟滚动就是「只渲染你看得见的部分」。

你屏幕高度就那么大,一次能显示50行数据,那我只渲染这50行。剩下的几万行?先存着,等你滚到了再说。

这样做的好处很明显:

  • DOM节点数量大幅减少——从几万个降到几十个
  • 渲染性能飙升——首屏加载秒开
  • 内存占用降低——浏览器不再崩溃

核心原理:监听滚动事件 → 计算当前可视区域 → 只渲染该区域内的数据 → 用绝对定位模拟滚动条高度

4.2 我自己常用的实现方案

我习惯把虚拟滚动分成三种场景,每种场景选不同的库:

场景 推荐方案 适用数据量
简单列表 react-window / vue-virtual-scroller 1万-10万
复杂表格 ag-Grid(虚拟滚动模式) 10万-100万
自定义大屏组件 自己手写虚拟滚动 灵活控制

我个人最常用的是 react-window,轻量、稳定、文档清晰。如果你用Vue,vue-virtual-scroller 也不错。

4.3 手写一个简易虚拟滚动

有时候库不够灵活,比如大屏上要自定义滚动条样式,或者要跟ECharts联动。这时候我建议自己写一个。

核心代码其实不长:

// 伪代码,理解核心逻辑即可
class VirtualScroll {
  constructor(container, data, itemHeight) {
    this.container = container;
    this.data = data;
    this.itemHeight = itemHeight;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
    this.startIndex = 0;
    this.endIndex = this.visibleCount;
    
    // 监听滚动
    container.addEventListener('scroll', () => {
      this.startIndex = Math.floor(container.scrollTop / itemHeight);
      this.endIndex = this.startIndex + this.visibleCount;
      this.render();
    });
  }
  
  render() {
    // 只渲染 startIndex 到 endIndex 的数据
    const visibleData = this.data.slice(this.startIndex, this.endIndex);
    // 用绝对定位把每一行放到正确的位置
    visibleData.forEach((item, index) => {
      const row = document.createElement('div');
      row.style.position = 'absolute';
      row.style.top = (this.startIndex + index) * this.itemHeight + 'px';
      row.textContent = item.name;
      this.container.appendChild(row);
    });
  }
}

小技巧:别忘了在滚动时复用DOM节点,而不是每次都创建新的。我一般会维护一个「节点池」,滚动时只更新内容,不增删节点。

4.4 避坑指南

虚拟滚动看着简单,实际坑不少。我踩过的几个:

我曾经踩过的坑:

  • 滚动闪烁——原因是滚动事件触发太频繁,渲染跟不上。解决方案:加个requestAnimationFrame做节流。
  • 行高不一致——如果每行高度不同,计算位置会出错。我一般用「预估高度+动态修正」的策略。
  • 搜索/筛选后数据变化——虚拟滚动依赖数据索引,数据变了要重新计算起始位置。记得在数据更新时重置滚动位置。

4.5 性能对比:虚拟滚动 vs 普通渲染

我拿一个真实项目的数据做过测试,10万行数据,每行5列:

指标 普通渲染 虚拟滚动
首屏渲染时间 3.2秒 0.3秒
DOM节点数 100,000+ ~60
滚动帧率 15 FPS(卡顿) 60 FPS(流畅)
内存占用 ~500MB ~50MB

差距一目了然。说白了,虚拟滚动就是「用计算换渲染」——多花点CPU算位置,少让浏览器去画DOM。

4.6 什么时候不适合用虚拟滚动?

嗯,这里要注意。虚拟滚动不是万能的。我遇到过几个场景,用了反而更糟:

  • 数据量小于500行——没必要,普通渲染就够了
  • 每行内容高度差异极大——比如有的行只有一行字,有的行有十行图,计算量太大
  • 需要全量搜索或排序——虚拟滚动只渲染部分数据,全量操作需要额外处理

遇到这些情况,我建议你换个思路。比如用「分页+懒加载」,或者干脆让后端做聚合计算,只返回汇总数据。

4.7 总结一下

虚拟滚动是我做大屏性能优化时最常用的手段之一。它不复杂,但细节很多。我的建议是:

  1. 先评估数据量,确定是否需要虚拟滚动
  2. 选一个成熟的库,别自己造轮子(除非你有特殊需求)
  3. 注意滚动闪烁、行高不一致、数据变化这三个坑
  4. 配合节流、节点复用等技巧,把性能压到极致

下一章我会讲「Canvas渲染:当DOM扛不住时,我们还有画布」。到时候我会分享一个用Canvas绘制10万点散点图的实战案例,保证让你大开眼界。