前端数据量一大,页面就卡成幻灯片,这事儿不少人都遇到过。尤其是表格、日志、或者像搜索建议下拉列表这种场景,后端一骨碌给你返回几万条、甚至十万条数据,如果直接v-for往页面上怼,浏览器基本就废了。我在实际项目里处理过一张接近十万行的流水明细表,当时用了一个专门针对这个问题的Vue组件库vue-virtual-scroll-list,把渲染性能从卡死边缘拉了回来,整个过程里的踩坑和调优,今天一次讲清楚。
这篇内容不是给你念文档,而是把我配置这个库、处理动态高度、应对异步数据、以及最终压测的完整过程都拆开讲。无论你是在做后台管理系统、实时日志展示,还是长列表聊天记录,只要你的列表数据量大到让页面喘不过气,这篇文章的思路和代码都能直接用。另外,标题里的关键点落在“高性能渲染”这几个字上,所以文里也会把虚拟滚动的原理讲明白,让你知道它为什么这么快,而不是只会调API。
1. 为什么直接渲染10万条数据会把页面拖垮
1.1 浏览器被几十万个DOM节点压垮的过程
先看一个很朴素的场景:接口返回了10万条数据,你用一个v-for把它们全部渲染成列表项。每个列表项如果只有简单的几行文字,那至少会生成10万个DOM节点,而每个节点又会有对应的属性、样式计算、布局信息。加上列表项内部的标签层级,整体DOM数量轻松上百万。这里面每一个环节,从createElement到appendChild,再到页面初次加载时的Layout和Paint,都是要命的时间消耗。
更麻烦的是,浏览器在用户滚动时,还需要持续维护这些节点的位置信息。哪怕你只是把页面往下拉一点,浏览器都要重新计算所有可见和不可见节点的几何位置。这种情况下,初始渲染时间会从几十毫秒飙升到几秒,滚动时的帧率也会掉到个位数,表现就是页面白屏很久,鼠标一滚就卡到怀疑人生。你用DevTools里的Performance面板去录一段,会看到主线程整段都是满的,全是Layout和Script的耗时。
1.2 真实场景:我遇到的那张流水表
我印象很深的一个项目是后台的资金流水管理页面。订单量大的时候,用户按时间范围筛选,后端一次查出来9万多条记录。最开始想得很简单,后端分页不就行了?但业务方要求前端必须支持一次性拉全量数据,然后在前端做二次过滤、排序和导出。
第一次搞的时候没经验,直接把数据扔给el-table加v-for,结果页面加载了大约4秒才出内容,往下滚动基本是PPT式滑动,Chrome还时不时弹一个“页面无响应”的提示。后来换成el-table自带的分页,用户体验又很分裂——用户想看中间某一段数据,得先一页一页翻半天,过滤逻辑也被截断成服务端来回传参,搞得接口改动很大。
被逼得没办法,才开始研究虚拟滚动方案。虚拟滚动的核心思路很简单:不管你总共有多少条数据,页面上始终只渲染视口范围内能看到的那几十条,滚动时动态替换渲染内容。这样10万条数据和100条数据,对DOM造成的压力是基本一样的。
2. 虚拟滚动方案选型与vue-virtual-scroll-list的优势
2.1 市面上常见的虚拟滚动方案对比
Vue生态里能实现虚拟滚动的方案其实不少,比较常见的有vue-virtual-scroller、vue-virtual-scroll-list,以及各UI组件库内置的虚拟列表(比如Element Plus的el-select开启filterable后内部就有)。还有一些人喜欢自己手写一个,通过scrollTop和clientHeight算起始索引。
我先说说我对这套方案的取舍。vue-virtual-scroller功能更全,支持组合式API、动态尺寸检测、水合优化之类的,但它的配置项相对复杂,而且它做了很多动态测量工作,在数据频繁更新时反而会有额外的性能开销。el-table的虚拟滚动在很长一段时间里只支持简单的行渲染,对树形表格、多级表头支持很别扭。
最终我选了vue-virtual-scroll-list,原因是它的设计足够纯粹:你给它一个数据源数组、一个列表项组件、一个唯一标识的key字段,它就把虚拟滚动这件事包了。它不关心你的数据长什么模样,也不侵入你的业务逻辑,和任何组件库都能搭配。体积还小,压缩后也就十来KB,没有额外依赖。对于很多“业务已经很复杂,不想再被框架束缚”的项目来说,这种解耦感特别重要。
2.2 版本兼容性与官方文档里没写全的坑
在Vue 2.x项目里,vue-virtual-scroll-list用得非常顺手,直接安装就能跑:
npm install vue-virtual-scroll-list --save然后在组件里注册:
import VirtualScrollList from 'vue-virtual-scroll-list' export default { components: { VirtualScrollList } }如果你用的是Vue 3,就要留意一点:这个库最初是给Vue 2设计的,虽然有人做了适配和fork版本,但直接用npm包名安装的版本在Vue 3下会遇到一些this.$createElement相关的兼容报错。Vue 3项目里我更建议用vue-virtual-scroller(对Vue 3有官方支持版本),或者干脆用组合式API自己封装一个简版虚拟列表。
我后来在另一个Vue 3项目里,就是用@vueuse/core里的useVirtualList组合式函数搞定的。不过今天这篇还是以vue-virtual-scroll-list为主线,因为很多老项目在用Vue 2,而且它的核心逻辑对理解虚拟滚动非常有帮助。
3. 核心用法与关键参数详解
3.1 三个必要参数:><template> <div class="list-wrapper"> <virtual-scroll-list :data-key="'id'" :data-sources="sources" :data-component="itemComponent" :keeps="30" :extra-props="{ onClickItem }" /> </div> </template> <script> import VirtualScrollList from 'vue-virtual-scroll-list' import Item from './Item.vue' export default { components: { VirtualScrollList }, data() { return { sources: [], itemComponent: Item } }, created() { // 模拟生成10万条数据 this.sources = Array.from({ length: 100000 }, (_, index) => ({ id: index, name: `用户${index}`, amount: (Math.random() * 10000).toFixed(2), time: new Date(Date.now() - index * 60000).toLocaleString() })) } } </script> <style scoped> .list-wrapper { height: 500px; overflow-y: auto; } </style>
列表项组件Item.vue是这样:
<template> <div class="item" @click="handleClick"> <span>{{ source.id }}</span> <span>{{ source.name }}</span> <span>{{ source.amount }}</span> <span>{{ source.time }}</span> </div> </template> <script> export default { props: { source: { type: Object, required: true }, index: { type: Number } }, methods: { handleClick() { this.onClickItem && this.onClickItem(source) } } } </script>看到没,这些都谈不上什么魔法,但关键是你要理解一件很核心的事情:列表项组件是被反复复用的。它不会为10万条数据创建10万个组件实例,而是最多创建几屏数量的实例,滚动时把source对象换成新数据。所以你在写列表项组件时,要尽量避免在里面放太重的东西(比如图片懒加载、复杂的计算属性、大量的watcher),否则复用的时候照样会拖慢。
3.2keeps、page-mode和extra-props的作用
keeps这个参数,可以理解为“缓冲区大小”。它决定了两件事:视口里要渲染多少个列表项,以及在视口之外上下各预留多少项用于缓冲。
我直观解释一下:如果你设置了keeps为30,而你的视口只能显示10项,那么实际渲染的DOM大概会是50项左右(顶部缓冲约20项加底部缓冲约20项再加上可见区域10项)。缓冲区的意义在于:当用户快速滚动或使用触控板惯性滚动时,新滚入视口的项已经被预先渲染好了,不会出现白屏、闪现的空白区域。
默认的keeps值是30,可以根据列表项的高度和视口高度调整。列表项越高、视口越矮,需要的keeps值越小。我一般用这个公式估算:
keeps ≈ Math.ceil(视口高度 / 列表项平均高度) * 2 + 10比如视口高度500px,列表项高度50px,那就是(500/50)*2+10 = 30。如果你拿不准,keeps=30基本不会出大问题。
extra-props,这个参数是用来向列表项组件传递额外props的。因为><virtual-scroll-list :data-key="'id'" :data-sources="sources" :data-component="itemComponent" :extra-props="{ onClickItem: handleItemClick, highlightKey: currentHighlightKey }" />
这样在Item.vue里就可以通过this.onClickItem来调用外部方法,this.highlightKey来读外部状态。这一步是真的容易踩坑,因为很多人会忘了传这个参数,然后在列表项组件里调用外部方法时发现是undefined。
再来说page-mode。这个参数为true时,虚拟列表不再要求外层固定高度和overflow-y: auto,而是直接监听整个页面的滚动事件。适合那种你希望列表撑开整个页面、滚动条在浏览器窗口层级而不是在某个内部容器里的场景。在移动端H5页面里挺常用。
<virtual-scroll-list page-mode :data-key="'id'" :data-sources="sources" :data-component="itemComponent" />3.3 外层容器高度和滚动容器必须搞明白
vue-virtual-scroll-list内部计算可见项的方式,是基于外层容器(或者page-mode下的window)的scrollTop和clientHeight来算的。所以它要求外层容器必须有一个确定的高度,不能是auto,否则内部计算会出一堆离谱结果。
如果用的是固定高度容器,记得给容器加上overflow-y: auto或scroll。如果你把滚动容器设成了内部某个子元素,那虚拟列表的监听对象就得跟着调整。这个库会默认寻找它的父组件作为滚动容器,但如果你外面套了多层div,而且滚动条出现在别的层上,就必须用scroll-target属性来指定滚动容器的选择器或元素。
我遇到过一种情况:外层父容器设置了height: 100%,但父容器本身在页面布局里高度没有被压紧,结果所有列表项都没渲染出来,页面一片空白。排查了半天发现,问题不在vue-virtual-scroll-list,而是父容器高度塌陷了。用了虚拟滚动,就要保证从根到滚动容器这一整条高度链都是闭合的,否则内部拿不到正确的clientHeight,列表渲染结果就是空白。
4. 10万条数据渲染的完整实操过程
4.1 准备测试数据:模拟接口返回10万条记录
直接从头搭一个可跑的示例来演示。为了方便演示,我们模拟一组资金流水的数据。数据字段包含id、name、amount、status和time,总条数正好10万条。为了更贴近真实场景,我在生成数据时还故意让内容有长有短,方便看动态高度测试下的表现。
// mockData.js const statusMap = ['成功', '处理中', '失败', '已退款'] export function generateMockData(count = 100000) { const list = [] for (let i = 0; i < count; i++) { list.push({ id: i, name: `流水单号 CF${String(i).padStart(8, '0')}`, amount: (Math.random() * 100000).toFixed(2), status: statusMap[i % 4], time: new Date(Date.now() - i * 86400000).toLocaleString() }) } return list }这里有个细节:用Array.from或者for循环生成大数组没问题,但不要在created钩子里去做额外的高耗时操作(比如对10万条数据做深拷贝、JSON.parse之类),否则数据准备阶段就会卡住,用户感受到的依然是一个漫长的白屏。
4.2 列表项组件的写法与注意事项
列表项组件不用花哨,但要注意一个问题:不要在template里写复杂的函数调用或过滤器。比如下面这种写法在大数据量下是很危险的:
<!-- 不推荐的写法 --> <span>{{ formatAmount(source.amount) }}</span> <span>{{ getStatusLabel(source.status) }}</span>原因是虚拟滚动复用组件实例时,会频繁更新source这个prop,每次更新都会重新触发模板里的这些函数计算。10万条数据滚动时,这些函数会被反复调用成千上万次,哪怕单次只有0.1毫秒,累计起来也会让滚动有明显的卡顿。
解决办法是,在数据生成时就把需要格式化好的字段预计算好,组件里直接显示,尽量不做运行时计算。
// 生成数据时直接格式化 list.push({ id: i, name: `流水单号 CF${String(i).padStart(8, '0')}`, amount: (Math.random() * 100000).toFixed(2), amountText: `¥ ${parseFloat(amount).toLocaleString()}`, status: statusMap[i % 4], time: new Date(Date.now() - i * 86400000).toLocaleString() })Item.vue里就纯粹做展示:
<template> <div class="log-item"> <div class="log-head"> <span class="log-name">{{ source.name }}</span> <span class="log-status" :class="statusClass">{{ source.status }}</span> </div> <div class="log-amount">{{ source.amountText }}</div> <div class="log-time">{{ source.time }}</div> </div> </template>4.3 页面组件完整代码与运行效果
页面组件里把虚拟列表和数据接起来:
<template> <div class="page-container"> <div class="toolbar"> <span>共 {{ sources.length }} 条记录</span> <button @click="prependData">前插100条</button> <button @click="removeFirstItem">删除首条</button> </div> <div class="list-container"> <virtual-scroll-list :data-key="'id'" :data-sources="sources" :data-component="itemComponent" :keeps="40" :extra-props="{ onItemClick: handleItemClick }" /> </div> </div> </template> <script> import VirtualScrollList from 'vue-virtual-scroll-list' import Item from './Item.vue' import { generateMockData } from './mockData' export default { name: 'VirtualListDemo', components: { VirtualScrollList }, data() { return { sources: [], itemComponent: Item, counter: 0 } }, created() { this.sources = generateMockData(100000) this.counter = 100000 }, methods: { handleItemClick(item) { console.log('clicked item:', item.id) }, prependData() { // 在列表头部插入新数据,验证data-key不是index时滚动位置是否正确 const newItems = [] for (let i = 0; i < 100; i++) { newItems.push({ id: `new_${this.counter++}`, name: `新流水 ${this.counter}`, amount: '100.00', amountText: '¥ 100.00', status: '处理中', time: new Date().toLocaleString() }) } this.sources = [...newItems, ...this.sources] }, removeFirstItem() { this.sources = this.sources.slice(1) } } } </script> <style scoped> .page-container { display: flex; flex-direction: column; height: 100vh; } .toolbar { height: 48px; line-height: 48px; padding: 0 16px; background: #fff; border-bottom: 1px solid #eee; } .list-container { flex: 1; overflow-y: auto; border: 1px solid #ebebeb; } </style>这段代码跑起来后,页面上会显示一个撑满剩余高度的列表,包含10万条数据。你不管怎么快速滚动,页面DOM数量始终稳定在屏幕可见项加缓冲区那一小部分。在DevTools的Console里执行document.querySelectorAll('.log-item').length,你会发现数量始终在60到100之间波动,而不是10万。
4.4 异步加载数据时的滚动位置保持
真实业务中,数据往往不是一次全量到位的。比如先加载第一页500条,用户滚动到底部再加载下一页。vue-virtual-scroll-list对动态追加数据是天然支持的,因为><template> <virtual-scroll-list ref="virtualListRef" :data-key="'id'" :data-sources="sources" :data-component="itemComponent" /> </template> <script> export default { methods: { async loadOlder() { const previousFirstId = this.sources[0]?.id const olderData = await fetchOlderData() this.sources = [...olderData, ...this.sources] this.$nextTick(() => { const index = this.sources.findIndex(item => item.id === previousFirstId) if (index > -1 && this.$refs.virtualListRef) { this.$refs.virtualListRef.scrollToIndex(index) } }) } } } </script>
5. 高动态列表场景:当列表项高度不固定时怎么办
5.1 固定高度和动态高度的本质区别
vue-virtual-scroll-list默认假设列表项的高度是固定的,或者说它先按一个预设的keeps和itemHeight估算渲染多少个。固定高度的优势是计算简单:只要知道scrollTop,就能直接算出当前应该从第几项开始渲染,公式就是Math.floor(scrollTop / itemHeight)。
动态高度则麻烦得多。比如一个列表项内容有多有少,有的只有一行字,有的有三行,有的里面还嵌了图片,那么每项的真实高度都不一样。浏览器只有把元素真正渲染出来、再measure一下,才知道多高。但虚拟列表为了省性能,只渲染可视区那一部分,所以它要维护一个“每项实际高度”的映射表。这个映射表是随着滚动不断更新和校准的。
这个库的处理方式比较聪明:它内部会维护一个position数组,记录每个列表项的偏移量和预估高度。当滚动到某一段时,它把预估高度作为初始值,等渲染完成后用真实高度覆盖回去。这样滚动过程中,高度数据会不断被修正,滚动位置会越来越准。
5.2 混排内容场景的处理经验与参数调整
如果你列表项里既有纯文本,又有图片、视频封面、状态标签这些不固定高度的内容,建议先把keeps调大一些。原因很简单:动态高度下,为了准确计算偏移量,库需要渲染更多项来做实际测量。keeps太小会导致频繁滚动时高度计算来不及修正,出现滚动条抖动、跳跃。
另外,遇到混排内容,尽量不要让列表项的高度依赖于图片的异步加载。图片没加载完成时高度为0,加载完成后突然撑高,这会直接导致可视区外的布局严重错位。我的做法是给图片容器一个固定的占位宽高比例(比如4:3),再用object-fit: cover裁剪图片,这样图片在加载前就有确定的高度,列表项高度始终稳定。
<template> <div class="feed-item"> <div class="text">{{ source.content }}</div> <div class="cover" v-if="source.cover"> <img :src="source.cover" alt="cover" /> </div> </div> </template> <style scoped> .cover { width: 100%; aspect-ratio: 4 / 3; background-color: #f0f0f0; border-radius: 6px; overflow: hidden; } .cover img { width: 100%; height: 100%; object-fit: cover; display: block; } </style>5.3item-primary属性和item-secondary属性的自定义用法
这个库还有一个不算出名但很实用的属性:item-primary和item-secondary。默认情况下,它会从>list.push({ id: i, content: text, height: text.length > 30 ? 80 : 50 })
这样虚拟列表在滚动到每一项时,至少有一个比较准确的高度基准,测量修正的压力就会小很多。真实项目里,如果列表项是稳定模板渲染出来的(比如每个卡片都是固定高度),压根不用管这两个字段。
6. 5项杀手级实践:从“能用”到“流畅”的调优记录
6.1 减少列表项组件的响应式开销
Vue的响应式系统在数据更新时有自己的开销。虚拟滚动复用一个组件实例时,它会用新数据替换source对象,这个替换本身不算太慢,但如果你在列表项组件里访问了source对象的很多字段,Vue需要为每个字段建立依赖收集和触发更新。
我在做那个流水表时,列表项里要展示的字段有5个,其中有一个字段是后端返回的长字符串备注。为了做行内省略显示,又在CSS里用了text-overflow: ellipsis。效果虽然还行,但滚动的时候总感觉有轻微掉帧。
后来排查发现,根本原因不是CSS,而是列表项组件里有一个计算属性用来判断状态颜色,判断逻辑里又依赖了另一个全局状态。每次source更新,计算属性就要重新算一遍,还得去读全局状态。我干脆取消了计算属性,把状态颜色在数据生成时就映射成一个statusClass字段,列表项组件里直接绑定类名,开销立刻降了一半。
能预先算好的字段,绝不在列表项组件里现算。这句话在虚拟滚动场景下再适用不过了。
6.2 图片懒加载与虚拟滚动配合时的坑
虚拟滚动本身已经只渲染可视区了,所以图片通常不需要再做懒加载——可视区外的图片压根不会创建DOM。但有一个坑:列表项里有远程图片时,快速滚动过程中会大量触发图片请求。因为组件滚动复用,同一个<img>节点的src属性被频繁替换,浏览器会对新src发起请求。如果用户从第1条快速滚到第1000条,可能瞬间发出去好几十个图片请求,带宽低的时候图片区域会一直闪空白。
我的解决方案是给<img>加一个loading="lazy"属性,让浏览器自己决定图片加载时机。这种方式和虚拟滚动不冲突,反而能进一步减少快速滚动时的图片请求数量。如果项目用的图片服务支持WebP,也建议在src上做格式切换,减小单张图片体积。
6.3 数据更新频率高时的批量更新策略
实时日志、股票行情这类列表,数据变化频率极高,可能每秒都有几十上百条新数据。如果每来一条就push到>let buffer = [] let timer = null function appendLog(item) { buffer.push(item) if (timer) return timer = setTimeout(() => { this.sources = this.sources.concat(buffer) buffer = [] timer = null }, 500) }
6.4 使用Object.freeze冻结静态列表数据
还有一个很多人忽略的优化点:如果10万条数据是一次性返回的,并且在列表中不会做增删改,那完全可以用Object.freeze把它冻结住。冻结后的对象不会被Vue转换成响应式,Vue遍历它做响应式处理的步骤会被省掉,>const rawList = generateMockData(100000) this.sources = Object.freeze(rawList.map(item => Object.freeze(item)))
这样做有个前提:你的列表项组件不需要修改source里的字段。如果你要在前端编辑列表项内容,就不能冻结,否则字段改了视图不更新。
我用这个技巧在同样的10万条数据场景下,把页面初始化的时间从900多毫秒降到了300毫秒左右。数据量越大,这个优化越明显。
6.5 合理使用scrollToIndex和搜索跳转
长列表通常都会搭配一个搜索定位的功能:用户输入一个单号,点搜索,列表自动滚动到对应位置。没有虚拟滚动前,这个功能很难做,因为浏览器加载10万条DOM后页面都卡死了,更别提滚动定位。有了虚拟滚动,就简单得多。
调用scrollToIndex的时候要注意一点:vue-virtual-scroll-list接受一个索引值,并且滚动到该索引所在位置是“瞬间直达”的,不会模拟平滑滚动动画。如果你想要平滑效果,得自己在外面做过渡处理,或者直接接受“瞬间定位”这种直来直去的交互——在大量数据场景下,瞬间定位反而是更符合用户预期的,至少比我见过一堆做了动画后滚个3秒才到的方案好得多。
7. 实测数据对比与性能调优记录
7.1 10万条数据直接渲染与虚拟滚动的对比数据
我在自己机器上做了一个简单的对比测试,数据量都是10万条,列表项结构完全一致。直接渲染的方式用了最简单的v-for;虚拟滚动方式用vue-virtual-scroll-list,keeps设为40。
测试环境:Chrome 126,CPU 8核,内存16GB,无其他大负载。数据如下:
| 指标 | 直接v-for渲染 | vue-virtual-scroll-list |
|---|---|---|
| 首次可交互时间 | 约4.2秒 | 约0.5秒 |
| DOM节点数量 | 约100万+ | 约80 |
| 内存占用(任务管理器) | 约480MB | 约60MB |
| 滚动帧率(快速滚动) | 每秒约3-5帧,明显卡顿 | 每秒约55-60帧,流畅 |
| 页面响应时间(点击按钮后) | 经常出现无响应卡死 | 未感觉延迟 |
这个对比表里的数据不是用专业设备测的,但能明显说明问题。虚拟滚动对长列表的优化是数量级上的,不是一点半点的提升。如果你还犹豫要不要上虚拟滚动,把表格翻给他看就够了。
7.2 从900ms到300ms:一次初始化性能优化记录
除了用Object.freeze,我还做了另外两个调整,把初始化和数据处理的耗时降了下来。
第一,提前生成所有需要展示的字段,绝不在created钩子里对10万条数据做二次遍历处理。例如,如果需要在列表里展示“状态中文描述”,就在生成模拟数据时直接写成中文,而不是在列表项组件里通过statusMap[source.status]去查。一次遍历10万条数据的耗时大概20毫秒,看着不多,但如果在组件渲染期间反复触发遍历,就会放大很多倍。
第二,把不需要响应式处理的业务数据放到this实例属性上,而不是data里。比如某些筛选条件、临时标记、配置map,这些只有列表项组件读取、但不在视图里绑定的数据,完全可以挂在this上,避免进入Vue的响应式系统。这个思路在处理大数据时非常有用。
经过这三项优化,10万条数据从接口返回(模拟)到页面完全可交互,总耗时大约从900毫秒降到了300毫秒。数值本身因机器而异,但优化的方向和幅度是明确的。
7.3 快速滚动、触摸板滚动、键盘滚动三种模式的表现差异
我还专门测试了不同类型的滚动操作。鼠标滚轮快速滚动时,虚拟滚动的表现很好,因为每次滚动触发的偏移量计算是同步的,渲染跟手。触摸板惯性滚动是一个常见问题——惯性滚动速度极快,中间会跳过很多帧,如果缓冲区不够大,列表底部会出现大片空白区域,等上一帧渲染完成后才补齐。
解决办法是增大keeps。我把keeps从30调到50后,触摸板快速滑动时的空白闪烁基本消失。代价是DOM节点数多了30%左右,但总节点数仍然只有100多个,完全在可接受范围内。
键盘滚动(比如用方向键或PageDown)的问题往往不在渲染,而在于焦点管理。如果你在列表项里放了可交互元素(按钮、链接),键盘滚动后焦点可能还停留在旧的可见项上,视觉上很违和。这里建议在列表容器上监听scroll事件,滚动时手动把焦点重置到容器本身。
8. 常见问题与排查技巧实录
8.1 表格形式排查:症状、原因与解法
这部分的经验,我整理成了一张速查表,方便你遇到问题时直接对照:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 页面一片空白,无任何列表项 | 外层容器高度塌陷,clientHeight为0 | 检查父元素高度链,给列表容器一个明确的height值 |
| 列表只显示开头几十条,滚动后显示开始错乱 | >handleScroll() { const el = this.$refs.scrollContainer if (el.scrollTop + el.clientHeight >= el.scrollHeight - 200) { this.loadMore() } }这时候虚拟滚动带来的好处是:即使已经加载了20万条数据,页面依然保持60帧的流畅度,因为每条数据只是一个JS对象,并不是一个真正的DOM节点。 9.2 把虚拟滚动封装成通用业务组件多个页面都要用大数据量列表时,建议把虚拟列表封装成一个带插槽的通用组件。 封装时注意不要破坏列表项组件的解耦性,最好通过props把渲染函数或组件传入,而不是在封装组件里硬编码列表项结构。这样业务方只需要传数据和渲染规则,就能复用整个滚动优化逻辑。 9.3 移动端长列表的特别注意事项移动端的视口小,单个列表项的屏占比大, 另外,移动端强烈建议配合 加载更多、下拉刷新与虚拟滚动的兼容方案最后分享一个我在做移动端长列表时踩过几次坑之后总结出的兼容方案。 下拉刷新场景里,用户习惯下拉后看到列表回到顶部。虚拟滚动下,这个“回顶部”操作其实很简单,调 0 选题推荐 - 网络与信息安全篇
毕业设计是大家学习生涯的最重要的里程碑,它不仅是对四年所学知识的综合运用,更是展示个人技术能力和创新思维的重要过程。选择一个合适的毕业设计题目至关重要,它应该既能体现你的专业能力,又能满…
/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
1. C语言学习路线规划对于初学者而言,掌握C语言需要系统性的学习路径。我建议将学习过程分为四个阶段:1.1 基础语法阶段(1-2周)这个阶段需要重点掌握:数据类型与变量(int、float、char等)运算符…
Cookiecutter Django 维护者指南:依赖自动化更新与 GitHub Actions 工作流全解析 【免费下载链接】cookiecutter-django Cookiecutter Django is a framework for jumpstarting production-ready Django projects quickly. 项目地址: https://gitcode.com/GitHub_…
SurfSense 前端实践:为 localStorage 数据加版本号并最小化存储的完整方案 【免费下载链接】SurfSense Open-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platfor…
GKE 多租户架构实战:基于命名空间的团队隔离、RBAC、资源配额与成本分摊指南 【免费下载链接】skills Agent Skills for Google products and technologies 项目地址: https://gitcode.com/GitHub_Trending/skills29/skills
本文以 Google Kubernetes Engine…
|