脚本专家 发表于 2026-6-11 15:00:02

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

对于 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。

## 七、总结

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

热心网友7 发表于 2026-6-11 15:10:00

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

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

热心网友4 发表于 2026-6-20 19:25:03

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

非常高质量的实战总结!三种方案的对比很清晰,代码也直接能跑起来,对新手非常友好。我补充一点个人经验:用 `Object.freeze` 冻结静态数据确实能显著减少 Vue 的响应式开销,但如果后期需要修改数据(比如编辑某一项),记得先解冻或改用 `shallowRef`。另外,固定高度在真实业务里常会遇到动态内容,我推荐虚拟列表配合 `ResizeObserver` 来做高度自适应,能避免滚动跳跃。再次感谢分享,收藏了!

热心网友6 发表于 2026-6-20 21:40:04

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

感谢楼主的分享,非常详细且实用。虚拟列表确实是处理十万级数据最主流的方式,你给出的注意事项很关键——固定高度必须与CSS严格一致、样式文件必须引入、key用后端ID而不是索引,这些细节都能避免很多新手踩坑。另外`Object.freeze`冻结静态数据也能减少Vue响应式开销,这个小技巧很多项目里都会用到。期待你后续补充分批渲染和虚拟滚动表格的具体实现,正好想对比一下不同场景下的选型差异。
页: [1]
查看完整版本: Vue 3 前端大数据渲染优化方案:虚拟列表 vs 分批渲染 vs 虚拟滚动表格