虚拟滚动列表

什么是虚拟滚动?

简单来说,虚拟滚动就是前端领域的一种 "按需渲染的障眼法"

我们都知道,浏览器的渲染能力是有局限的。如果把十万条数据直接用 v-for 全部画成真实的 DOM 节点,浏览器瞬间就会陷入假死状态。因为庞大的节点树会榨干内存,你稍微动一下滚动条,浏览器的重排(Reflow)和重绘(Repaint)成本高得离谱。

虚拟滚动的诞生就很好地解决了这个问题。它的核心思路特别朴素:既然用户的屏幕就那么大,一次最多只能看到十几条日志,那我干嘛要把剩下的九万多条全画出来?

打个比方:

没有虚拟滚动的列表 = 铺地毯,不管屋子多大,你得把十万米长的地毯全铺开,累死浏览器。

有虚拟滚动的列表 = 探照灯(滑动窗口),数据是一个长长的黑屋子,屏幕就是探照灯,灯照到哪(滑到哪),我们就只渲染哪里的十几条数据。

虚拟滚动可以解决什么问题?

  1. 首次加载白屏时间长

    如果不做优化,浏览器要一口气创建几万个 DOM,页面卡白屏几秒钟是常态。用了虚拟滚动,不管总数据是一万条还是一百万条,首屏永远只渲染那二十来个 DOM,真正做到毫秒级首屏加载。

  2. 滚动极其卡顿

    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 配合占位图进行优化,或者降低滚动的触发频率(节流)。

相关推荐
祯民2 小时前
《复合型 AI Agent 开发:从理论到实践》实体书上架
前端
NEXT062 小时前
深拷贝与浅拷贝的区别
前端·javascript·面试
不写八个2 小时前
PixiJS教程(一):快速搭建环境启动项目
前端·pixijs
PieroPc2 小时前
用html+css+js 写一个Docker 教程
javascript·css·docker·html
菜鸟小芯3 小时前
【GLM-5 陪练式前端新手入门】第二篇:CSS 让网页从 “能用” 变 “好看”
前端·css
We་ct3 小时前
LeetCode 112. 路径总和:两种解法详解
前端·算法·leetcode·typescript
倚肆3 小时前
WebSocket连接教程示例(Spring Boot + STOMP + SockJS + Vue)
vue.js·spring boot·websocket
Hello.Reader3 小时前
Tauri 项目结构前端壳 + Rust 内核,怎么协作、怎么构建、怎么扩展
开发语言·前端·rust
Cache技术分享3 小时前
331. Java Stream API - Java Stream 实战案例:找出合作最多的作者对
前端·后端