尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

vue-virtual-scroll-list实战:10万条数据高性能虚拟滚动渲染方案

vue-virtual-scroll-list实战:10万条数据高性能虚拟滚动渲染方案 如果你在管理后台里渲染过一张两万行的表格多半体会过拖动滚动条时白屏、卡顿、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-listnext或者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-list2.2 组件注册和核心props安装完成后在入口文件里全局注册或者在需要的组件里局部注册都行。我习惯局部注册因为只有大列表页面才需要它。import VirtualList from vue-virtual-scroll-list export default { components: { VirtualList } }这个库的props设计看着有点“长”但核心就这几个prop作用是否必填>template div classrow-item span classrow-id{{ item.id }}/span div classrow-content p classrow-title{{ item.title }}/p p classrow-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 classlist-container virtual-list :data-keyid :data-sourceslist :data-componentRowItem :size48 :keeps20 classlist / /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-keyid :data-sourceslist :data-componentRowItem :size48 :keeps20 tobottomloadMore /在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 )或者重新给数组赋值一个新数组。这样一来组件内部会因为>
返回列表