查看: 88|回复: 1

Vue 3 前端大数据渲染优化方案:虚拟列表 vs 分批渲染 vs 虚拟滚动表格

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
对于 Vue 开发而言,一次性渲染十万条数据会导致 DOM 节点暴增、频繁重排重绘、主线程阻塞,最终页面卡顿甚至崩溃。常规 v-for 直接输出十万条数据是不可行的。本文提供三种经过工业验证的优化方案,均基于 Vue 3 实现,涵盖虚拟列表、分批渲染和虚拟滚动表格,并附带完整可落地代码及异常处理细节。

## 一、为什么直接渲染十万条数据会卡顿?

浏览器单个页面建议承载 DOM 节点不超过 1000 个。当一次性渲染十万条数据时:
- 十万个 DOM 元素占用大量内存,浏览器处理缓慢;
- Vue 响应式批量更新触发多次重排重绘;
- JS 执行与 DOM 渲染在同一线程,渲染阻塞导致页面无响应。

核心优化思路:减少实际渲染的 DOM 数量,只渲染可视区域内的内容,或分批渲染。

## 二、方案一:虚拟列表(首选,工业级方案)

### 原理

只渲染当前可视区域内的列表项,通过滚动偏移量动态计算需要渲染的范围,始终保持页面中仅有几十个 DOM 节点。

### 实现(使用 vue-virtual-scroller 插件)

步骤 1:安装插件
  1. npm install vue-virtual-scroller@next --save
复制代码

若安装失败可尝试 cnpm 或 yarn。Vue 2 请使用版本 1.0.10+,并额外安装 @vue/composition-api。

步骤 2:全局注册(main.ts)
  1. import { createApp } from 'vue';
  2. import App from './App.vue';
  3. import VueVirtualScroller from 'vue-virtual-scroller';
  4. import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
  5. const app = createApp(App);
  6. app.use(VueVirtualScroller, {
  7.   itemSize: 50,
  8.   buffer: 200,
  9.   windowResizeDebounce: 100
  10. });
  11. app.mount('#app');
复制代码

样式文件必须引入,否则渲染会错乱。全局 itemSize 可被局部覆盖。buffer 建议 200-300px,避免滚动空白。

步骤 3:页面使用
  1. <template>
  2.   <div style="height: 500px; overflow-y: auto; border: 1px solid #eee;">
  3.     <RecycleScroller
  4.       class="scroller"
  5.       :items="bigList"
  6.       :item-size="50"
  7.       key-field="id"
  8.       :buffer="200"
  9.       @scroll="handleScroll"
  10.     >
  11.       <template #default="{ item }">
  12.         <div class="list-item" @click="handleItemClick(item)">
  13.           <span>{{ item.id }}</span> <span>{{ item.name }}</span> <span>{{ item.content }}</span>
  14.         </div>
  15.       </template>
  16.       <template #empty>
  17.         <div>暂无数据</div>
  18.       </template>
  19.       <template #loading>
  20.         <div>数据加载中...</div>
  21.       </template>
  22.     </RecycleScroller>
  23.   </div>
  24. </template>
  25. <script setup lang="ts">
  26. import { ref, onMounted, onUnmounted } from 'vue';
  27. const bigList = ref([]);
  28. const generateData = () => {
  29.   const data = [];
  30.   for (let i = 1; i <= 100000; i++) {
  31.     data.push({ id: i, name: `测试数据${i}`, content: `这是Vue渲染十万条数据的测试内容,序号${i}` });
  32.   }
  33.   return data;
  34. };
  35. onMounted(async () => {
  36.   try {
  37.     // 实际项目请改为接口请求,优先使用 Object.freeze 静态数据
  38.     bigList.value = Object.freeze(generateData());
  39.   } catch (error) {
  40.     console.error('数据加载失败:', error);
  41.     // 使用 ElMessage.error 提示
  42.   }
  43. });
  44. onUnmounted(() => {
  45.   bigList.value = [];
  46. });
  47. </script>
  48. <style scoped>
  49. .scroller { height: 100%; }
  50. .list-item { height: 50px; line-height: 50px; border-bottom: 1px solid #eee; padding: 0 20px; display: flex; align-items: center; cursor: pointer; }
  51. .list-item:hover { background-color: #f5f5f5; }
  52. </style>
复制代码

关键优化点
- 固定列表项高度:item-size 必须与 CSS height 严格一致,否则偏移计算错误。不固定高度时需启用 dynamic-item-size 属性。
- key-field 必须使用唯一标识(优先后端 ID),不要用索引。
- 容器必须设置固定高度和 overflow-y: auto。
- 列表项模板内避免复杂计算和 v-if,图片使用懒加载。

## 三、方案二:分批渲染(简单,无插件依赖)

### 原理

将十万条数据分成小批次(例如每批 100 条),通过 setTimeout 或 requestAnimationFrame 分批追加到渲染数组,每次批处理后使用 nextTick 等待 DOM 更新,再处理下一批。

### 实现
  1. <template>
  2.   <div>
  3.     <div style="height: 600px; overflow-y: auto; border: 1px solid #eee;">
  4.       <div class="list-item" v-for="item in renderList" :key="item.id">
  5.         <span>{{ item.id }}</span> <span>{{ item.name }}</span> <span>{{ item.content }}</span>
  6.       </div>
  7.     </div>
  8.     <div v-if="isLoading">加载中...({{ renderList.length }}/100000)</div>
  9.     <div v-if="isLoadFail" @click="retryRender">加载失败,点击重试</div>
  10.     <div v-if="!isLoading && !isLoadFail && renderList.length === bigList.length">已全部加载完成</div>
  11.   </div>
  12. </template>
  13. <script setup lang="ts">
  14. import { ref, onMounted, onUnmounted, nextTick } from 'vue';
  15. let bigList = [];
  16. const renderList = ref([]);
  17. const isLoading = ref(true);
  18. const isLoadFail = ref(false);
  19. const batchSize = ref(100);
  20. const delay = ref(20);
  21. let renderTimer = null;
  22. const generateData = () => {
  23.   const data = [];
  24.   for (let i = 1; i <= 100000; i++) {
  25.     data.push({ id: i, name: `测试数据${i}`, content: `这是Vue分批渲染十万条数据的测试内容,序号${i}` });
  26.   }
  27.   return data;
  28. };
  29. const batchRender = async (data, start = 0) => {
  30.   try {
  31.     const end = Math.min(start + batchSize.value, data.length);
  32.     await nextTick(() => {
  33.       renderList.value.push(...data.slice(start, end));
  34.     });
  35.     if (end < data.length) {
  36.       if (renderTimer) clearTimeout(renderTimer);
  37.       renderTimer = setTimeout(() => batchRender(data, end), delay.value);
  38.     } else {
  39.       isLoading.value = false;
  40.     }
  41.   } catch (error) {
  42.     isLoading.value = false;
  43.     isLoadFail.value = true;
  44.   }
  45. };
  46. const retryRender = () => {
  47.   isLoadFail.value = false;
  48.   isLoading.value = true;
  49.   renderList.value = [];
  50.   batchRender(bigList);
  51. };
  52. onMounted(async () => {
  53.   try {
  54.     // 低性能设备可动态调整 batchSize 和 delay
  55.     bigList = generateData();
  56.     await batchRender(bigList);
  57.   } catch (error) {
  58.     isLoading.value = false;
  59.     isLoadFail.value = true;
  60.   }
  61. });
  62. onUnmounted(() => {
  63.   if (renderTimer) clearTimeout(renderTimer);
  64.   bigList = [];
  65.   renderList.value = [];
  66. });
  67. </script>
  68. <style scoped>
  69. .list-item { height: 50px; line-height: 50px; border-bottom: 1px solid #eee; padding: 0 20px; display: flex; align-items: center; }
  70. </style>
复制代码

关键优化点
- batchSize 建议 100~200 条,低性能设备可降至 50。
- delay 建议 10~30ms,性能差时适当增大。
- 使用 push(...data) 批量追加,配合 nextTick 确保 DOM 更新完成。
- 组件卸载时必须清除定时器,避免内存泄漏。
- 实际项目中推荐后端分页接口,前端分批请求,而不是一次性拉取全部数据。

## 四、方案三:虚拟滚动表格(适配表格场景)

### 原理

对于需要表格展示大量数据(如数据报表)的场景,同样采用虚拟滚动思想:只渲染可视区域的行,通过计算滚动偏移动态替换行内容。可借助 Element Plus 的 el-table 结合自定义虚拟滚动实现,或使用专门的表格虚拟滚动组件(如 vue-table-virtual-scroll)。

### 实现思路

Element Plus 的 el-table 本身不支持十万行数据的虚拟滚动,但可以通过 el-table-v2(2.5.0+ 版本提供)实现。以下是一个基于 el-table-v2 的简化示例:
  1. <template>
  2.   <el-table-v2
  3.     :columns="columns"
  4.     :data="bigList"
  5.     :width="800"
  6.     :height="500"
  7.     :estimated-row-height="50"
  8.     fixed
  9.   />
  10. </template>
  11. <script setup lang="ts">
  12. import { ref } from 'vue';
  13. const columns = [
  14.   { key: 'id', title: 'ID', width: 80 },
  15.   { key: 'name', title: '名称', width: 150 },
  16.   { key: 'content', title: '内容', width: 400 }
  17. ];
  18. const bigList = ref([]);
  19. const generateData = () => {
  20.   const data = [];
  21.   for (let i = 1; i <= 100000; i++) {
  22.     data.push({ id: i, name: `测试数据${i}`, content: `内容${i}` });
  23.   }
  24.   return data;
  25. };
  26. bigList.value = generateData();
  27. </script>
复制代码

更多参数如 row-class、header-class 等可参考 Element Plus 文档。el-table-v2 自带虚拟滚动,无需额外计算。

## 五、方案对比与选型建议

- 虚拟列表(vue-virtual-scroller):推荐用于十万条以上长列表、电商商品列表、日志列表。性能最优,但需要插件依赖。
- 分批渲染:适合中小型项目、需求简单、不想引入额外依赖的场景,如后台简单日志。但体验稍差,用户会看到数据逐步出现。
- 虚拟滚动表格(el-table-v2):专门用于表格场景,如数据报表、后台管理系统。官方组件,稳定可靠,但有 Element Plus 版本要求。

## 六、通用优化技巧

- 使用 Object.freeze() 冻结静态数据,减少 Vue 响应式开销。
- 组件销毁时及时释放引用和定时器。
- 列表项内避免复杂 v-if/v-for 嵌套,用 v-show 替代 v-if。
- 图片或媒体资源使用懒加载。
- 后端建议提供分页接口,前端分批请求数据,避免一次性传输十万条 JSON。

## 七、总结

三种方案均能实现十万条数据无卡顿渲染。虚拟列表是工业级首选,分批渲染适合快速迭代,虚拟滚动表格针对表格场景。开发者应根据实际需求(是否需要表格、是否允许插件依赖、目标设备性能)选择合适方案。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Vue 3 前端大数据渲染优化方案:虚拟列表 vs 分批渲染 vs 虚拟滚动表格

感谢楼主的分享,对 Vue 虚拟列表的实现讲解得很清晰,特别是 `item-size` 必须与 CSS 高度一致这个细节,实际开发中很容易忽略。准备去试试 `vue-virtual-scroller`,期待后续分批渲染和虚拟滚动表格的讲解。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-11 17:01 , Processed in 0.028802 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部