对于 Vue 开发而言,一次性渲染十万条数据会导致 DOM 节点暴增、频繁重排重绘、主线程阻塞,最终页面卡顿甚至崩溃。常规 v-for 直接输出十万条数据是不可行的。本文提供三种经过工业验证的优化方案,均基于 Vue 3 实现,涵盖虚拟列表、分批渲染和虚拟滚动表格,并附带完整可落地代码及异常处理细节。
## 一、为什么直接渲染十万条数据会卡顿?
浏览器单个页面建议承载 DOM 节点不超过 1000 个。当一次性渲染十万条数据时:
- 十万个 DOM 元素占用大量内存,浏览器处理缓慢;
- Vue 响应式批量更新触发多次重排重绘;
- JS 执行与 DOM 渲染在同一线程,渲染阻塞导致页面无响应。
核心优化思路:减少实际渲染的 DOM 数量,只渲染可视区域内的内容,或分批渲染。
## 二、方案一:虚拟列表(首选,工业级方案)
### 原理
只渲染当前可视区域内的列表项,通过滚动偏移量动态计算需要渲染的范围,始终保持页面中仅有几十个 DOM 节点。
### 实现(使用 vue-virtual-scroller 插件)
步骤 1:安装插件
- npm install vue-virtual-scroller@next --save
复制代码
若安装失败可尝试 cnpm 或 yarn。Vue 2 请使用版本 1.0.10+,并额外安装 @vue/composition-api。
步骤 2:全局注册(main.ts)
- import { createApp } from 'vue';
- import App from './App.vue';
- import VueVirtualScroller from 'vue-virtual-scroller';
- import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
- const app = createApp(App);
- app.use(VueVirtualScroller, {
- itemSize: 50,
- buffer: 200,
- windowResizeDebounce: 100
- });
- app.mount('#app');
复制代码
样式文件必须引入,否则渲染会错乱。全局 itemSize 可被局部覆盖。buffer 建议 200-300px,避免滚动空白。
步骤 3:页面使用
- <template>
- <div style="height: 500px; overflow-y: auto; border: 1px solid #eee;">
- <RecycleScroller
- class="scroller"
- :items="bigList"
- :item-size="50"
- key-field="id"
- :buffer="200"
- @scroll="handleScroll"
- >
- <template #default="{ item }">
- <div class="list-item" @click="handleItemClick(item)">
- <span>{{ item.id }}</span> <span>{{ item.name }}</span> <span>{{ item.content }}</span>
- </div>
- </template>
- <template #empty>
- <div>暂无数据</div>
- </template>
- <template #loading>
- <div>数据加载中...</div>
- </template>
- </RecycleScroller>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, onUnmounted } from 'vue';
- const bigList = ref([]);
- const generateData = () => {
- const data = [];
- for (let i = 1; i <= 100000; i++) {
- data.push({ id: i, name: `测试数据${i}`, content: `这是Vue渲染十万条数据的测试内容,序号${i}` });
- }
- return data;
- };
- onMounted(async () => {
- try {
- // 实际项目请改为接口请求,优先使用 Object.freeze 静态数据
- bigList.value = Object.freeze(generateData());
- } catch (error) {
- console.error('数据加载失败:', error);
- // 使用 ElMessage.error 提示
- }
- });
- onUnmounted(() => {
- bigList.value = [];
- });
- </script>
- <style scoped>
- .scroller { height: 100%; }
- .list-item { height: 50px; line-height: 50px; border-bottom: 1px solid #eee; padding: 0 20px; display: flex; align-items: center; cursor: pointer; }
- .list-item:hover { background-color: #f5f5f5; }
- </style>
复制代码
关键优化点
- 固定列表项高度:item-size 必须与 CSS height 严格一致,否则偏移计算错误。不固定高度时需启用 dynamic-item-size 属性。
- key-field 必须使用唯一标识(优先后端 ID),不要用索引。
- 容器必须设置固定高度和 overflow-y: auto。
- 列表项模板内避免复杂计算和 v-if,图片使用懒加载。
## 三、方案二:分批渲染(简单,无插件依赖)
### 原理
将十万条数据分成小批次(例如每批 100 条),通过 setTimeout 或 requestAnimationFrame 分批追加到渲染数组,每次批处理后使用 nextTick 等待 DOM 更新,再处理下一批。
### 实现
- <template>
- <div>
- <div style="height: 600px; overflow-y: auto; border: 1px solid #eee;">
- <div class="list-item" v-for="item in renderList" :key="item.id">
- <span>{{ item.id }}</span> <span>{{ item.name }}</span> <span>{{ item.content }}</span>
- </div>
- </div>
- <div v-if="isLoading">加载中...({{ renderList.length }}/100000)</div>
- <div v-if="isLoadFail" @click="retryRender">加载失败,点击重试</div>
- <div v-if="!isLoading && !isLoadFail && renderList.length === bigList.length">已全部加载完成</div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, onUnmounted, nextTick } from 'vue';
- let bigList = [];
- const renderList = ref([]);
- const isLoading = ref(true);
- const isLoadFail = ref(false);
- const batchSize = ref(100);
- const delay = ref(20);
- let renderTimer = null;
- const generateData = () => {
- const data = [];
- for (let i = 1; i <= 100000; i++) {
- data.push({ id: i, name: `测试数据${i}`, content: `这是Vue分批渲染十万条数据的测试内容,序号${i}` });
- }
- return data;
- };
- const batchRender = async (data, start = 0) => {
- try {
- const end = Math.min(start + batchSize.value, data.length);
- await nextTick(() => {
- renderList.value.push(...data.slice(start, end));
- });
- if (end < data.length) {
- if (renderTimer) clearTimeout(renderTimer);
- renderTimer = setTimeout(() => batchRender(data, end), delay.value);
- } else {
- isLoading.value = false;
- }
- } catch (error) {
- isLoading.value = false;
- isLoadFail.value = true;
- }
- };
- const retryRender = () => {
- isLoadFail.value = false;
- isLoading.value = true;
- renderList.value = [];
- batchRender(bigList);
- };
- onMounted(async () => {
- try {
- // 低性能设备可动态调整 batchSize 和 delay
- bigList = generateData();
- await batchRender(bigList);
- } catch (error) {
- isLoading.value = false;
- isLoadFail.value = true;
- }
- });
- onUnmounted(() => {
- if (renderTimer) clearTimeout(renderTimer);
- bigList = [];
- renderList.value = [];
- });
- </script>
- <style scoped>
- .list-item { height: 50px; line-height: 50px; border-bottom: 1px solid #eee; padding: 0 20px; display: flex; align-items: center; }
- </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 的简化示例:
- <template>
- <el-table-v2
- :columns="columns"
- :data="bigList"
- :width="800"
- :height="500"
- :estimated-row-height="50"
- fixed
- />
- </template>
- <script setup lang="ts">
- import { ref } from 'vue';
- const columns = [
- { key: 'id', title: 'ID', width: 80 },
- { key: 'name', title: '名称', width: 150 },
- { key: 'content', title: '内容', width: 400 }
- ];
- const bigList = ref([]);
- const generateData = () => {
- const data = [];
- for (let i = 1; i <= 100000; i++) {
- data.push({ id: i, name: `测试数据${i}`, content: `内容${i}` });
- }
- return data;
- };
- bigList.value = generateData();
- </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。
## 七、总结
三种方案均能实现十万条数据无卡顿渲染。虚拟列表是工业级首选,分批渲染适合快速迭代,虚拟滚动表格针对表格场景。开发者应根据实际需求(是否需要表格、是否允许插件依赖、目标设备性能)选择合适方案。 |