前端开发攻略---vue3长列表性能优化终极指南:虚拟滚动、分页加载、时间分片等6种方案详解与代码实现

前端开发攻略---vue3长列表性能优化终极指南:虚拟滚动、分页加载、时间分片等6种方案详解与代码实现 方案一虚拟滚动最推荐、效果最明显虚拟滚动的核心思想是无论列表有多长只渲染用户可视区域的那几条数据。当用户滚动时动态计算并更新渲染的数据。在 Vue 3 中推荐使用vue-virtual-scroller或vueuse/core结合组合式 API 手动实现。1. 使用第三方库vue-virtual-scroller这是一个专门用于虚拟滚动的库适配 Vue 3。安装npm install vue-virtual-scrollernext # 或者 yarn add vue-virtual-scrollernext引入样式和组件在main.js中全局引入或直接在组件内引入。// main.js import { createApp } from vue import VueVirtualScroller from vue-virtual-scroller import vue-virtual-scroller/dist/vue-virtual-scroller.css // 必须引入样式 import App from ./App.vue const app createApp(App) app.use(VueVirtualScroller) app.mount(#app)组件中使用template div classdemo-container !-- RecycleListScroller: 复用DOM性能最佳 -- RecycleListScroller classscroller :itemslist :item-size50 key-fieldid v-slot{ item } div classitem spanID: {{ item.id }} - 内容: {{ item.content }}/span /div /RecycleListScroller /div /template script setup import { ref } from vue; // 生成1万条测试数据 const list ref(Array.from({ length: 10000 }, (_, index) ({ id: index, content: 这是第 ${index} 条数据 }))); /script style scoped .demo-container { height: 500px; /* 父容器必须有固定高度 */ border: 1px solid #ccc; } .scroller { height: 100%; } .item { height: 50px; /* 必须与 item-size 一致 */ line-height: 50px; padding: 0 10px; border-bottom: 1px solid #eee; } /style关键点必须设置容器高度。:item-size必须等于每个列表项的高度如果是动态高度需要使用DynamicScroller。2. 手动实现简易虚拟滚动理解原理如果不想引入第三方库可以使用vueuse/core结合滚动事件手动实现。template div refcontainerRef classvirtual-list-container scrollhandleScroll !-- 占位元素用于撑开滚动条 -- div :style{ height: totalHeight px }/div !-- 绝对定位的可见区域 -- div classvirtual-list-phantom :style{ transform: translateY(${offsetY}px) } div v-foritem in visibleData :keyitem.id classlist-item :style{ height: itemSize px } {{ item.content }} /div /div /div /template script setup import { ref, computed, onMounted } from vue; const props defineProps({ listData: { type: Array, default: () [] }, itemSize: { type: Number, default: 50 }, // 每个项目固定高度 containerHeight: { type: Number, default: 500 } // 容器高度 }); // 生成数据 const allData ref(Array.from({ length: 10000 }, (_, i) ({ id: i, content: Item ${i} }))); const containerRef ref(null); const scrollTop ref(0); // 计算总高度 const totalHeight computed(() allData.value.length * props.itemSize); // 计算可见区域显示的数量 const visibleCount computed(() Math.ceil(props.containerHeight / props.itemSize) ); // 计算起始索引 const startIndex computed(() Math.floor(scrollTop.value / props.itemSize) ); // 计算结束索引多渲染几个作为缓冲防止白屏 const endIndex computed(() Math.min( startIndex.value visibleCount.value 2, // 2 作为缓冲 allData.value.length ) ); // 偏移量让列表永远紧贴顶部 const offsetY computed(() startIndex.value * props.itemSize); // 实际渲染的数据 const visibleData computed(() allData.value.slice(startIndex.value, endIndex.value) ); // 滚动事件处理 const handleScroll (e) { scrollTop.value e.target.scrollTop; }; /script style scoped .virtual-list-container { height: v-bind(containerHeight px); overflow-y: auto; position: relative; border: 1px solid #ccc; } .virtual-list-phantom { position: absolute; top: 0; left: 0; width: 100%; } .list-item { display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; padding: 0 10px; background-color: white; } /style方案二分页加载传统但有效如果长列表的目的就是让用户浏览全部数据但用户通常只看前几页可以使用分页 懒加载上拉加载更多。template div classinfinite-list scrollhandleScroll div v-foritem in displayedData :keyitem.id classlist-item {{ item.content }} /div !-- 加载中状态 -- div v-ifloading classloading加载中.../div !-- 没有更多了 -- div v-if!hasMore classno-more没有更多数据了/div /div /template script setup import { ref, computed, onMounted } from vue; // 原始数据假设有1万条 const allData ref(Array.from({ length: 10000 }, (_, i) ({ id: i, content: Item ${i} }))); // 分页状态 const pageSize 20; const currentPage ref(1); const loading ref(false); // 当前渲染的数据计算属性只显示当前页的数据 const displayedData computed(() { return allData.value.slice(0, currentPage.value * pageSize); }); // 是否还有更多 const hasMore computed(() displayedData.value.length allData.value.length); // 模拟加载更多 const loadMore () { if (loading.value || !hasMore.value) return; loading.value true; // 模拟异步加载 setTimeout(() { currentPage.value; loading.value false; }, 300); }; // 滚动到底部加载 const handleScroll (e) { const { scrollTop, scrollHeight, clientHeight } e.target; if (scrollTop clientHeight scrollHeight - 10) { loadMore(); } }; /script style scoped .infinite-list { height: 500px; overflow-y: auto; border: 1px solid #ccc; } .list-item { height: 50px; line-height: 50px; border-bottom: 1px solid #eee; padding: 0 10px; } .loading, .no-more { text-align: center; padding: 10px; color: #999; } /style方案三使用v-once或keep-alive静态数据优化如果列表项中大部分内容是不变的静态文案、图片链接可以使用v-once指令让 Vue 只渲染一次之后视为静态内容跳过更新。template div v-foritem in hugeList :keyitem.id v-once !-- 这部分内容不会随数据变化而更新 -- img :srcitem.img altstatic span{{ item.name }}/span !-- 注意如果 item.name 后续会变这里不会更新需慎用 -- /div /template适用场景日志展示、历史记录、静态配置项等。方案四使用shallowRef/shallowReactive减少深层响应式监听Vue 3 的响应式系统默认是深度的。对于长列表如果我们只关心列表整体替换而不关心列表内某个对象属性的变化可以使用浅层响应式。script setup import { shallowRef } from vue; // 使用 shallowRef 替代 ref // allData.value 的变化会被监听到但 allData.value[0].name 的变化不会被监听到 const allData shallowRef([]); // 获取数据 const fetchData async () { const res await fetch(/api/long-list); const data await res.json(); // 整体替换触发视图更新 allData.value data; }; // 如果某个子项内部的属性变化Vue 不会自动检测 // 如果需要更新某一项必须整体替换数组或对象 const updateItem (index, newItem) { const newArray [...allData.value]; newArray[index] newItem; allData.value newArray; // 触发更新 }; fetchData(); /script优点省去了对一万个对象内部属性的getter/setter初始化开销内存占用更低初始化更快。方案五时间分片requestIdleCallback如果必须一次性渲染大量 DOM例如报表导出预览为了避免阻塞主线程导致页面卡死可以将渲染任务拆分成多个小任务在浏览器空闲时执行。注意Vue 3 的异步渲染队列已经做了一些优化但手动分片可以进一步优化。template div div v-foritem in renderedItems :keyitem.id {{ item.content }} /div /div /template script setup import { ref, onMounted } from vue; const allItems ref([]); const renderedItems ref([]); onMounted(() { // 假设有 10w 条数据 const bigData Array.from({ length: 100000 }, (_, i) ({ id: i, content: Item ${i} })); allItems.value bigData; // 分片渲染 const total bigData.length; const chunkSize 100; // 每次渲染 100 条 let index 0; function appendChunk() { if (index total) return; // 使用 requestIdleCallback 在空闲时执行或者使用 setTimeout // 这里使用 setTimeout 兼容性更好 setTimeout(() { const chunk bigData.slice(index, index chunkSize); renderedItems.value.push(...chunk); index chunkSize; appendChunk(); // 递归处理下一块 }, 0); // 或者使用 requestIdleCallback } appendChunk(); }); /script综合对比与选择建议方案适用场景复杂度性能提升虚拟滚动极长列表1000需要流畅滚动中⭐⭐⭐⭐⭐分页加载列表不是必须一次性展示完适合移动端低⭐⭐⭐v-once静态内容、纯展示列表低⭐⭐shallowRef数据只整体替换不修改内部属性低⭐⭐⭐时间分片初始化时需要渲染巨量DOM且必须全渲染高⭐⭐⭐最佳实践组合首选虚拟滚动shallowRef。既能保证滚动流畅又能降低响应式系统开销。如果业务无法接受虚拟滚动例如需要 SEO 或需要精确的高度计算退而求其次使用分页加载。