虚拟滚动实战技巧与性能优化全解析

UP主~红瑞 优化 阅读 1,987
赞 11 收藏
二维码
手机扫码查看
反馈

又踩坑了,虚拟滚动竟然卡成PPT

最近在做一个数据展示的项目,列表项可能达到几千条。直接渲染的话页面直接卡死,于是我果断选择了虚拟滚动。但万万没想到,这个看似简单的功能让我折腾了整整两天。

虚拟滚动实战技巧与性能优化全解析

一开始用的是社区推荐的一个虚拟滚动库,刚上线就收到用户反馈:滚动特别卡顿,简直像在看PPT。这里我踩了个坑,以为是库本身的问题,换了两个库还是同样的问题。最后发现其实是我的使用方式有问题。

排查过程:从怀疑库到发现问题

我先怀疑是库的性能问题,毕竟开源项目质量参差不齐。换了一个star数更高的库,结果还是一样卡。后来我又怀疑是样式问题,把所有的CSS都去掉测试,发现性能还是不行。

折腾了半天才发现,问题出在我给每一行绑定的事件监听器上。因为列表项太多,每个item都绑定了click事件,导致浏览器压力山大。这里给大家提个醒:千万不要给大量DOM元素直接绑定事件

核心代码就这几行

最终解决方案是结合IntersectionObserver和事件委托。说起来简单,但调优的过程真是让人头大。下面是完整的核心代码:

class VirtualScroll {
  constructor(container, options) {
    this.container = container;
    this.itemHeight = options.itemHeight;
    this.total = options.total;
    this.renderItem = options.renderItem;

    this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight);
    this.bufferCount = 3; // 额外渲染的缓冲区
    this.startIndex = 0;
    this.endIndex = this.visibleCount + this.bufferCount;

    this.items = [];
    this.init();
  }

  init() {
    this.container.style.position = 'relative';
    this.container.style.overflowY = 'auto';
    this.container.style.height = ${this.visibleCount * this.itemHeight}px;

    this.content = document.createElement('div');
    this.content.style.position = 'absolute';
    this.content.style.top = '0';
    this.content.style.left = '0';
    this.content.style.width = '100%';
    this.container.appendChild(this.content);

    this.render();
    this.addEventListeners();
  }

  render() {
    const fragment = document.createDocumentFragment();
    for (let i = this.startIndex; i < this.endIndex; i++) {
      let item = this.items[i];
      if (!item) {
        item = document.createElement('div');
        item.style.height = ${this.itemHeight}px;
        item.dataset.index = i;
        item.className = 'virtual-item';
        this.items[i] = item;
      }
      item.innerHTML = this.renderItem(i);
      fragment.appendChild(item);
    }
    this.content.innerHTML = '';
    this.content.appendChild(fragment);
  }

  addEventListeners() {
    this.container.addEventListener('scroll', () => this.onScroll());
    
    // 使用事件委托处理点击事件
    this.container.addEventListener('click', (e) => {
      const target = e.target.closest('.virtual-item');
      if (target) {
        const index = target.dataset.index;
        console.log(Clicked item ${index});
        // 这里可以执行具体业务逻辑
      }
    });
  }

  onScroll() {
    const scrollTop = this.container.scrollTop;
    this.startIndex = Math.floor(scrollTop / this.itemHeight);
    this.endIndex = this.startIndex + this.visibleCount + this.bufferCount;
    this.content.style.transform = translateY(${this.startIndex * this.itemHeight}px);
    this.render();
  }
}

// 使用示例
const container = document.querySelector('#scroll-container');
new VirtualScroll(container, {
  itemHeight: 50,
  total: 10000,
  renderItem: (index) => Item ${index}
});

几个优化点值得说说

这段代码有几个关键优化点要说一下:

  • 事件委托:通过在容器上监听事件,避免了为每个item单独绑定事件
  • 缓冲区设计:多渲染几个item作为缓冲,防止快速滚动时出现空白
  • transform替代top:使用transform做位移比直接修改top性能更好
  • documentFragment:批量插入DOM比逐个appendChild性能高很多

还有些小瑕疵

虽然主要问题解决了,但目前还有两个小问题:一是快速滚动到底部时偶尔会闪一下空白;二是当item高度不固定时需要额外计算。不过这些问题影响不大,后续有时间再优化吧。

以上是我个人对这个虚拟滚动的完整讲解,有更优的实现方式欢迎评论区交流。这种性能优化的技巧还有很多拓展用法,后面我会继续分享这类实战经验。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论