我用原生JavaScript写了个虚拟滚动列表,性能惊喜
最近在做一个数据看板的需求,后端一口气甩过来两万条日志记录,要求在前端渲染成可滚动的列表。一开始用最简单的 `map` 全部渲染,结果页面直接卡成幻灯片,滚动一下要等半天。试了试手头的虚拟滚动库,不是体积太大就是 API 不顺手,索性自己用原生 JavaScript 写了一个,没想到性能出乎意料地好,滚动起来丝滑得像原生应用。
虚拟滚动的核心原理
说白了,虚拟滚动就是一个障眼法:不管数据有多少条,DOM 里永远只保留可视区域附近的那几十个节点。用一个外层容器撑出总高度,让浏览器的滚动条看起来像在滚一个很长的列表;然后监听 `scroll` 事件,算出当前应该显示哪一段数据,再更新列表内容。
关键点在于两点:一是总高度模拟,二是可视区节点复用。前者用 `padding` 或者一个空的占位 div 实现都行,后者则需要尽量减少创建和销毁 DOM 的次数。
我是怎么实现的
我选了一个比较朴素但稳健的方案:固定行高。每条记录高度是 32px,这个前提让计算变得非常简单。
const rowHeight = 32;
const visibleCount = Math.ceil(containerHeight / rowHeight) + 5; // 多渲染几个缓冲区
滚动时只需要:
const scrollTop = container.scrollTop;
const startIndex = Math.floor(scrollTop / rowHeight);
const endIndex = startIndex + visibleCount;
然后从数据数组里截取这一段,用 `innerHTML` 或者 `textContent` 拼进列表。外层容器用 `padding-top` 和 `padding-bottom` 把高度撑开,这样滚动条的长度就对了。
这里有个小坑:`scroll` 事件触发频率极高,直接操作 DOM 会导致抖动。我用了 `requestAnimationFrame` 做节流,保证每一帧最多更新一次。实测下来,两万条数据滚动时 CPU 占用很低,完全没有卡顿。
性能对比:惊喜在哪里
写完之后我忍不住对比了三种方案:全量渲染、使用某个开源虚拟滚动组件、自己写的原生实现。
- 全量渲染:初始化耗时 1.6 秒,滚动时帧率常常掉到 10 帧以下。
- 开源组件:初始化也不慢,滚动还算流畅,但打包体积增加了 80KB,而且为了适配我的固定行高需求,还得改配置。
- 原生实现:初始化时间不到 100ms,滚动全程稳定在 60 帧,内存占用更是天壤之别——全量渲染时 DOM 节点数接近两万,虚拟滚动后只有不到三十个。
最让我意外的不是快,而是内存的改善。在 Chrome 的 Performance 面板里看,全量渲染那一下堆内存飙升了 40MB,虚拟滚动则几乎是一条水平线。对于需要长期挂在页面上的看板来说,这比初始加载快慢更重要。
另一个惊喜是代码量。整个组件压缩后不到 3KB,不用处理框架的生命周期,也没有外部依赖,复制到任何项目里都能直接用。对普通场景来说,真没必要为了一个列表去引入重型的交互库。
有哪些坑和优化空间
当然,原生实现也有局限。如果行高不固定,就得在渲染后测量实际高度并缓存偏移量,复杂度会上升。另外,如果列表项有复杂的交互事件,最好用事件委托,避免给每个动态添加的节点绑监听器。
更精细的优化还可以做二分查找来定位起始索引,或者用 `content-visibility` 辅助首屏渲染。不过对我来说,固定行高加 `requestAnimationFrame` 已经足够满足业务需求了。
虚拟列表最核心的价值不是炫技,而是提醒我们:在性能敏感的场景里,少一点 DOM,就少一点卡顿。如果你也遇到长列表的渲染瓶颈,不妨自己动手写一个试试——实现一次绝对会比背一百遍八股文更加深刻。
这次经历让我明白,原生 JavaScript 不是过时的技能,反而能在关键时刻给你最直接的掌控感。当你亲手把两万行数据“缝”进几十个节点里,看到滚动条流畅得像流水一样时,那种满足感,真的挺惊喜。
管理员
黑卡会员