虚拟滚动实战技巧与性能优化全解析
又踩坑了,虚拟滚动竟然卡成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立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。

暂无评论