如果你在管理后台里渲染过一张两万行的表格,多半体会过拖动滚动条时白屏、卡顿、CPU风扇狂转的感受。数据量一旦到10万条,常规的v-for渲染已经不是卡,而是直接拖死页面。我这篇文章就从一个真实的工单列表场景说起,讲讲怎么用vue-virtual-scroll-list把10万条数据做成流畅滚动的高性能渲染列表,包括选型、安装、参数计算、踩坑记录和性能调优,尽量做到看完了就能自己上手改。
先说结论:10万条数据在浏览器里完全能跑,但前提是DOM只渲染可视区域那几十个节点,而不是把10万条数据全部拍进页面。vue-virtual-scroll-list就是帮你做这件事的。下面我按从原理到实战的顺序拆开讲。
1. 先搞清楚:10万条数据为什么会卡
1.1 一次性渲染的三大瓶颈:DOM数量、布局、内存
很多同学第一次面对大数据列表,第一反应是“我改成虚拟滚动就好了”,但真让他说清楚为什么卡,又讲不明白。这里我用自己的实测数据说明一下。
假如10万条数据,每行是一个简单的div,包含两个<span>。直接v-for渲染出来的DOM节点数大概是30万个左右。浏览器拿到这30万个节点后要做三件事:
- 创建节点:每个DOM节点本质上是一个C++对象,30万个对象一次性创建,光这一下就要几百毫秒。
- 样式计算和布局:节点越多,CSS匹配和
reflow的时间越长,而且只要你滚一下,浏览器就得重新评估哪些区域需要绘制。 - 内存占用:30万个节点占用的内存轻松超过100MB,低端设备直接触发GC卡顿。
我做了一次简单对比:同样的10万条数据,用v-for渲染后,Chrome的DOM Count大概是30万,页面可交互时间是3.2秒,滚动时FPS掉到个位数;改成vue-virtual-scroll-list后,DOM节点数稳定在30个左右,滚动时FPS能到50以上。这个差距就是虚拟列表存在的意义。
1.2 虚拟列表的核心思想:按需渲染,视觉欺骗
虚拟列表的原理并不神秘。你滚动一个长列表时,真正落在视口里的行数其实很少。假设每行48px,视口高度600px,那么视口里最多同时显示13行。既然只看得见13行,那就只渲染13行,其余的用一块总高度撑起来,让滚动条看起来还是10万条的长度。
具体过程是:
- 外层容器高度固定,设置
overflow-y: auto。 - 内层有一个“幽灵容器”,高度设为
总条数 × 行高,用来撑出正确的滚动条。 - 实际内容区绝对定位或使用
transform: translateY(),根据scrollTop实时偏移。 - 滚动时计算当前可视区对应的数据区间
[startIndex, endIndex],只渲染这个区间内的行。
用公式表示就是:
可视条数 visibleCount = ceil(视口高度 / 行高) 起始索引 startIndex = floor(scrollTop / 行高) 结束索引 endIndex = startIndex + visibleCount + 缓冲条数 我的经验是:虚拟列表最大的坑不是“怎么渲染”,而是“怎么处理高度”。如果每行高度固定,实现起来非常简单;如果每行高度不固定,后面会单独讲。 ### 1.3 为什么选用vue-virtual-scroll-list而不是自己造轮子 自己实现一个极简虚拟列表,其实也就几十行代码,但用到生产环境就会发现有一堆边界情况: - 滚动事件触发频率很高,需要`requestAnimationFrame`合并渲染; - 快速滚动时可能出现白屏,需要上下多渲染几行作为缓冲; - 列表数据变化时要保持滚动位置不跳变; - 动态高度、异步加载、页面滚动模式、keep-alive缓存等场景都要处理。 `vue-virtual-scroll-list`把这些已经封装好了。它在选择上比较轻量,props设计也很符合Vue习惯,我只需要关注数据和行组件,不用维护一整套滚动计算逻辑。如果你只是需要一个固定行高、高性能的大列表,用它基本没有心智负担。 ## 2. vue-virtual-scroll-list 选型与基础环境 ### 2.1 安装前的版本确认 先泼一盆冷水:`vue-virtual-scroll-list`这个大版本流行于Vue 2时代。如果你的项目是Vue 2,直接`npm install`就能用。如果是Vue 3项目,需要先确认你装到的版本是否支持Vue 3,或者是否更推荐使用社区适配过的fork,比如`vue-virtual-scroll-list@next`或者`vue-virtual-scroller`。 我的建议是:项目用的是Vue 2,直接上官方包;项目是Vue 3,装之前看一眼`package.json`里的`peerDependencies`,或者直接去npm仓库看版本发布时间。千万不要在Vue 3项目里装了一个老Vue 2的包然后抱怨组件不渲染,这种问题八成是版本不兼容。 在我的演示环境里,用的就是最经典的Vue 2 + vue-virtual-scroll-list组合。安装命令很简单: ```bash npm install vue-virtual-scroll-list # 或者 yarn add vue-virtual-scroll-list
2.2 组件注册和核心props
安装完成后,在入口文件里全局注册,或者在需要的组件里局部注册都行。我习惯局部注册,因为只有大列表页面才需要它。
import VirtualList from 'vue-virtual-scroll-list' export default { components: { VirtualList } }
这个库的props设计看着有点“长”,但核心就这几个:
| prop | 作用 | 是否必填 |
|---|
><template> <div class="row-item"> <span class="row-id">{{ item.id }}</span> <div class="row-content"> <p class="row-title">{{ item.title }}</p> <p class="row-desc">{{ item.desc }}</p> </div> </div> </template> <script> export default { name: 'RowItem', props: { item: { type: Object, required: true } } } </script> <style scoped> .row-item { display: flex; align-items: center; height: 48px; padding: 0 16px; box-sizing: border-box; border-bottom: 1px solid #f0f0f0; } .row-id { width: 80px; color: #999; font-size: 12px; } .row-title { margin: 0; font-size: 14px; line-height: 20px; } .row-desc { margin: 0; font-size: 12px; color: #999; line-height: 18px; } </style>然后在父组件里使用: <template> <div class="list-container"> <virtual-list :data-key="'id'" :data-sources="list" :data-component="RowItem" :size="48" :keeps="20" class="list" /> </div> </template> <script> import VirtualList from 'vue-virtual-scroll-list' import RowItem from './components/RowItem.vue' export default { name: 'App', components: { VirtualList, RowItem }, data() { return { list: Array.from({ length: 100000 }, (_, index) => ({ id: index, title: `第 ${index} 条数据`, desc: `这是编号为 ${index} 的数据描述` })) } } } </script> <style> html, body, #app { height: 100%; margin: 0; } .list-container { height: 100%; padding: 16px; box-sizing: border-box; } .list { height: 100%; overflow: auto; border: 1px solid #eee; border-radius: 8px; } </style>
注意三点: - 行组件里的根节点高度必须和
size保持一致,这里是48px,如果行内有边框,一定要加上box-sizing: border-box,否则实际高度会变成50px,滚动位置会漂移。 ><virtual-list :data-key="'id'" :data-sources="list" :data-component="RowItem" :size="48" :keeps="20" @tobottom="loadMore" />在loadMore里追加数据: async loadMore() { const page = this.page + 1 const newData = await fetchList(page) this.list = this.list.concat(newData) this.page = page }
注意:一定要给list重新赋值,不能直接this.list.push(...)。否则这个组件内部检测数据源变化的方式可能不会触发更新。我自己在这里踩过坑,后面第5章会细说。 定位到指定行也很常见,比如“从URL参数带过来一个id,需要滚动到这一条”。vue-virtual-scroll-list可以通过start和offset配合实现。比如要定位到第5000条数据,可以设置start: 5000,首次渲染时会从第5000条开始;如果要从顶部滚动到5000条的位置,可以用offset: 5000 * 48,但offset一般只在组件内部做初始同步,使用时需要多测几次。 如果组件没有提供直接跳转的方法,更通用的做法是:给滚动容器打一个ref,然后手动设置scrollTop = targetIndex * rowHeight,再触发一次自己的虚拟列表更新。这是我比较推荐的方式,可控性更强。 4. 性能实测与进一步优化4.1 用Performance面板看真实渲染成本代码跑起来后,别急着说“很流畅”,打开Chrome DevTools的Performance面板实测一下。我这里记录了几个关键指标: - 页面初次加载,
vue-virtual-scroll-list渲染10万条数据的JS执行时间大约是80ms,这个时间花在生成数据源上,不是花在DOM渲染上。 - 快速滚动时,长任务(Long Task)几乎没有,每帧执行时间稳定在20ms以内,也就是FPS在50~60之间。
- Memory面板观察,DOM节点数量稳定在20个左右,堆内存占用主要是数据源本身,DOM部分的泄放问题基本不存在。
对比普通v-for渲染30万个DOM节点,初次加载JS执行时间能到3秒以上,滚动时每帧都有超过100ms的长任务,肉眼可见的卡顿。从这两个数据能看出来,虚拟列表的收益非常直观。 4.2 4个容易被忽略的性能细节- 避免在行组件里绑定大量事件:每个行组件自己绑定
click、mouseenter事件是可以的,但不要绑全局事件、不要用window.addEventListener,因为组件创建销毁频繁,忘记解绑很容易内存泄漏。 - 谨慎使用
index作为>this.list = this.list.map((item, index) => index === 0 ? { ...item, name: '新名字' } : item )或者重新给数组赋值一个新数组。这样一来,组件内部会因为>
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设
2026/9/14 21:19:43
1. Java SPI机制概述Java SPI(Service Provider Interface)是Java提供的一种服务发现机制,它允许第三方为某个接口提供实现,并在运行时动态加载这些实现。这种机制在JDBC、日志框架等场景中广泛应用,是Java模块化设计的…
李华
网站建设
2026/9/14 21:17:53
绵阳网站托管踩坑实录:3家服务商对比评测,告别拖更噩梦 改个需求建站公司拖一周,这种痛谁懂?上个月,绵阳某机械厂的王总找我吐槽,说他们的官网改个联系电话,对方技术说要走流程,结果等了5天还没动静。这还没完,网站突然打不开,打电话没人接,发邮件石沉大海。王总当时就急了:“我花几万块买的服务,就这?”…
李华
网站建设
2026/9/14 21:17:25
戏剧与影视学写毕业论文,最容易踩的大坑就是通篇复述影片剧情。 很多同学熬几个月写完初稿,交给导师直接打回:文献综述只是罗列导演生平,理论套用生硬,影片文本分析浮于表面,创新点空洞没有支撑。戏影论文讲…
李华
网站建设
2026/9/14 21:16:56
1. 项目背景与核心价值主从配电网的优化控制一直是电力系统领域的重要研究方向。传统集中式优化方法在面对大规模分布式电源接入时,往往面临计算复杂度高、通信负担重、隐私保护难等问题。而ADMM(交替方向乘子法)作为一种经典的分布式优化算法…
李华
网站建设
2026/9/14 21:16:40
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
李华
网站建设
2026/9/14 21:16:22
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
李华
|