什么是虚拟滚动?
简单来说,虚拟滚动就是前端领域的一种 "按需渲染的障眼法"。
我们都知道,浏览器的渲染能力是有局限的。如果把十万条数据直接用 v-for 全部画成真实的 DOM 节点,浏览器瞬间就会陷入假死状态。因为庞大的节点树会榨干内存,你稍微动一下滚动条,浏览器的重排(Reflow)和重绘(Repaint)成本高得离谱。
虚拟滚动的诞生就很好地解决了这个问题。它的核心思路特别朴素:既然用户的屏幕就那么大,一次最多只能看到十几条日志,那我干嘛要把剩下的九万多条全画出来?
打个比方:
没有虚拟滚动的列表 = 铺地毯,不管屋子多大,你得把十万米长的地毯全铺开,累死浏览器。
有虚拟滚动的列表 = 探照灯(滑动窗口),数据是一个长长的黑屋子,屏幕就是探照灯,灯照到哪(滑到哪),我们就只渲染哪里的十几条数据。
虚拟滚动可以解决什么问题?
-
首次加载白屏时间长
如果不做优化,浏览器要一口气创建几万个 DOM,页面卡白屏几秒钟是常态。用了虚拟滚动,不管总数据是一万条还是一百万条,首屏永远只渲染那二十来个 DOM,真正做到毫秒级首屏加载。
-
滚动极其卡顿
DOM 数量少了,内存占用直线下降,滚动时的渲染压力几乎为零。页面就像抹了德芙一样丝滑。
虚拟滚动和传统分页有什么区别?
你可能会问:不就是数据多吗?我直接做个分页器(一页 50 条,点击下一页)或者触底加载(滑到底部再请求 50 条)不行吗?
这几种方案各有优劣,简单对比一下:
一句话总结:分页是"切块看",触底加载是"越背越重",而虚拟滚动是"轻装上阵的障眼法"。海量数据下,虚拟滚动是唯一的解。
虚拟滚动的核心概念
在讲工作原理之前,先搞清楚页面上的三个核心"盒子",不然后面可能看不懂。
可视区: 就是用户能看到的那个固定高度的框,必须带个 overflow-y: auto 产生滚动条。它的任务是监听滚动距离。
占位区: 这是一个看不见的空盒子。既然我们只渲染十几条数据,原本的滚动条肯定很短。为了骗过用户,我们需要用 总数据量 × 每条高度 算出总高度,把这个空盒子撑得极高,造出一个极其逼真的超长滚动条。
真实渲染区: 这里面放的才是真正循环出来的那十几条真实 DOM。当用户滑动时,我们要用 CSS 的 transform: translateY 把这个盒子死死拽在用户的视线范围内。
虚拟滚动的工作原理
虚拟滚动的工作原理主要分为三步,我们一步一步来看。
第一步:初始化与占位(备货阶段)
这一步是在页面刚渲染时完成的。
拿到后台返回的 10 万条数据数组。
假设每条日志高度固定是 50px,可视区高度是 500px。
计算占位区总高度:100,000 * 50 = 5,000,000px。把滚动条硬生生撑开。
计算屏幕能放下几条数据:500 / 50 = 10 条。
第二步:监听滚动与数据截取(找数据)
当用户滚动鼠标时,触发 scroll 事件。
拿到当前滚动条卷去的高度 scrollTop(比如用户向下滑了 500px)。
计算起始索引 (startIndex):500 / 50 = 10。说明现在该从第 10 条数据开始显示了。
计算结束索引 (endIndex):起始索引 + 屏幕容量 = 10 + 10 = 20。
用 slice(10, 20) 从十万条总数据里,精准切出这 10 条数据塞给 Vue 的 v-for 去渲染。
第三步:动态偏移(挪位置)
这是最关键的一步。因为你往下滚了 500px,真实的渲染盒子其实已经被顶到视口上方去了(看不见了)。
所以你需要计算一个偏移量 (offsetY):startIndex * 50 = 500px。
然后给真实渲染区加上样式 transform: translateY(500px),把它强行拽回可视区里。
这里贴一段纯粹的 Vue 3
虚拟滚动常见的问题
问题1:滑动过快导致白屏闪烁
当你鼠标滑轮滚得飞快,浏览器的 JS 计算和 DOM 渲染稍微慢了半拍,列表的底部或者顶部就会出现一瞬间的空白。
建议(引入缓冲区): 不要在截取数据时抠得那么死。可视区能放 10 条,你可以在上面多截 5 条,下面多截 5 条作为缓冲(Buffer)。也就是 slice(startIndex - 5, endIndex + 5)。这样就算滚得快,也有缓冲数据顶着,视觉上平滑很多。
问题2:列表项高度不固定(动态高度)
上面的例子我们假设了日志高度都是 50px,这叫定高虚拟滚动,非常简单。但实际 AI 项目里,有的对话只有一句"你好",有的对话是一整篇几千字的文章,高度根本不固定!
一旦高度不固定,你算不出总高度,也算不出具体的偏移量,整个系统就崩溃了。
建议: 这是虚拟滚动的深水区。通常的解法是**"预估高度 + 真实渲染后更新缓存"**。先假设所有项都是 50px,等节点真实渲染到页面上后,利用 ResizeObserver 或者 getBoundingClientRect 获取它的真实高度,然后更新到一个专门记录高度的数组里,再重新计算偏移。如果你们用 Vue,强烈推荐直接上成熟的库,比如 vue-virtual-scroller。
问题3:里面包含了复杂的组件
如果你截取出来的那 10 条数据里,包含了极度复杂的图表(Echarts)或者大量的图片,在快速滚动时疯狂销毁和重建这些复杂组件,依然会导致明显的卡顿。
建议: 虚拟滚动里面尽量保持 DOM 结构的简单。如果非要放复杂图表,考虑用 CSS visibility: hidden 配合占位图进行优化,或者降低滚动的触发频率(节流)。