
核心是利用 vxe-table 内置的大数据优化特性(虚拟滚动、懒加载等)+ 前端渲染优化
官网地址:https://vxetable.cn/#/demo/list
安装
javascript
npm install vxe-table@next
核心优化:开启「虚拟滚动」(解决大数据渲染卡顿)
vxe-table 的 虚拟滚动 是解决大数据量(万级以上)卡顿的关键:只渲染「可视区域内的行 / 列」,而非全部数据,大幅减少 DOM 节点数量。
基础虚拟滚动(行虚拟)
适用于行数多、列数少的场景
javascript
<template>
<!-- 开启行虚拟滚动:设置 scroll-y 的 enabled + 固定高度 -->
<vxe-table
ref="xTableRef"
v-model:data="tableData"
:columns="tableColumns"
:scroll-y="{
enabled: true, // 开启行虚拟滚动
height: 600, // 固定表格高度(必须,虚拟滚动依赖固定高度)
// 可选:缓冲区大小(可视区域外预渲染的行数,默认5)
reserve: 5
}"
border
/>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { VxeTable, VxeTableProps } from 'vxe-table';
import 'vxe-table/lib/style.css';
// 模拟10万条大数据(实际从接口获取)
const tableData = ref<Array<{ id: number; name: string; age: number }>>([]);
// 列配置(TS类型提示)
const tableColumns = ref<VxeTableProps.Columns>([
{ field: 'id', title: 'ID', width: 100 },
{ field: 'name', title: '名称', width: 200 },
{ field: 'age', title: '年龄', width: 100 }
]);
// 初始化10万条数据
onMounted(() => {
const mockData = Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
age: Math.floor(Math.random() * 50) + 18
}));
tableData.value = mockData;
});
</script>
列虚拟滚动(行列双虚拟)
适用于行列都很多(比如列数超过 50)的场景,同时开启「行虚拟 + 列虚拟」:
javascript
<template>
<vxe-table
v-model:data="tableData"
:columns="tableColumns"
<!-- 行虚拟 -->
:scroll-y="{ enabled: true, height: 600 }"
<!-- 列虚拟:设置 scroll-x 的 enabled + 固定宽度 -->
:scroll-x="{
enabled: true, // 开启列虚拟滚动
width: '100%', // 表格宽度(必须)
reserve: 3 // 列缓冲区大小
}"
border
/>
</template>
<script setup lang="ts">
// 模拟多列(比如100列)
const tableColumns = ref<VxeTableProps.Columns>([
{ field: 'id', title: 'ID', width: 100 },
// 动态生成100列
...Array.from({ length: 100 }, (_, i) => ({
field: `col${i}`,
title: `列${i + 1}`,
width: 150
}))
]);
</script>
进阶优化:数据懒加载(解决一次性加载大数据卡顿)
如果数据量超过 10 万条,即使虚拟滚动,一次性加载全量数据也会占用大量内存,建议结合 滚动懒加载(滚动到底部时加载下一批数据)。
实现滚动懒加载(结合虚拟滚动)
javascript
<template>
<vxe-table
ref="xTableRef"
v-model:data="tableData"
:columns="tableColumns"
:scroll-y="{ enabled: true, height: 600 }"
@scroll="handleTableScroll"
border
/>
<!-- 加载中提示 -->
<div v-if="isLoading" class="loading">加载中...</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { VxeTable, VxeTableInstance } from 'vxe-table';
const xTableRef = ref<VxeTableInstance | null>(null);
const tableData = ref<Array<any>>([]);
const tableColumns = ref<VxeTableProps.Columns>([/* 列配置 */]);
const isLoading = ref(false);
let currentPage = 1;
const PAGE_SIZE = 2000; // 每次加载2000条(根据性能调整)
// 初始化加载第一页
onMounted(() => loadMoreData());
// 滚动到底部时加载下一页
const handleTableScroll = () => {
const tableEl = xTableRef.value?.$el.querySelector('.vxe-table--body-wrapper');
if (!tableEl || isLoading.value) return;
const { scrollTop, scrollHeight, clientHeight } = tableEl;
// 滚动到底部(留100px缓冲区)
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreData();
}
};
// 加载更多数据
const loadMoreData = async () => {
isLoading.value = true;
try {
// 模拟接口请求(实际替换为你的接口)
const res = await mockApiRequest(currentPage, PAGE_SIZE);
// 追加数据(虚拟滚动下直接push即可)
tableData.value = [...tableData.value, ...res.data];
currentPage++;
} finally {
isLoading.value = false;
}
};
// 模拟接口请求
const mockApiRequest = (page: number, size: number) => {
return new Promise<{ data: Array<any> }>((resolve) => {
setTimeout(() => {
const data = Array.from({ length: size }, (_, i) => ({
id: (page - 1) * size + i + 1,
name: `用户${(page - 1) * size + i + 1}`,
age: Math.floor(Math.random() * 50) + 18
}));
resolve({ data });
}, 300);
});
};
</script>
额外性能优化点
优化列配置(减少不必要的渲染)
- 缓存列配置:用 computed 缓存列配置,避免每次渲染重新生成:
javascript
import { computed } from 'vue';
const tableColumns = computed<VxeTableProps.Columns>(() => [
{ field: 'id', title: 'ID', width: 100 },
// ...其他列
]);
- 避免复杂自定义渲染:减少 renderCell/formatter 中的复杂计算,提前处理数据:
javascript
// 不好:渲染时计算
{
field: 'age',
title: '年龄',
formatter: ({ row }) => `年龄:${row.age}岁` // 简单计算可以,但复杂逻辑提前处理
}
// 好:提前处理数据
tableData.value = mockData.map(item => ({
...item,
ageText: `年龄:${item.age}岁` // 提前计算好
}));
{ field: 'ageText', title: '年龄' }
关闭不必要的表格功能
关闭 vxe-table 中不需要的交互 / 样式功能,减少 DOM 操作和事件监听:
javascript
<vxe-table
:show-header-overflow="false" // 关闭表头溢出提示
:show-body-overflow="false" // 关闭单元格溢出提示
:highlight-hover-row="false" // 关闭行hover高亮(减少DOM类名切换)
:highlight-current-row="false"// 关闭当前行高亮
:border="false" // 不需要边框时关闭
:loading="false" // 不需要loading时关闭
/>
优化数据更新方式
- 避免直接修改 tableData,使用 vxe-table 提供的 loadData 方法(内部做了性能优化):
javascript
xTableRef.value?.loadData(newData); // 比直接赋值 tableData.value = newData 更高效
- 批量更新数据:如果需要修改多条数据,用 setData 批量更新,减少重渲染次数:
javascript
xTableRef.value?.setData((row) => {
if (row.id === 100) {
return { ...row, name: '新名称' };
}
return row;
});
事件防抖 / 节流
对表格的 sort-change、filter-change 等频繁触发的事件做防抖 / 节流:
javascript
import { debounce } from 'lodash-es';
// 排序事件防抖(500ms触发一次)
const handleSortChange = debounce((params) => {
// 处理排序逻辑
}, 500);
VxeTableList组件
javascript
<!--
* @Description:
* @Author: qianlishi
* @Date: 2025-12-05 10:17:53
* @LastEditors: Do not edit
* @LastEditTime: 2025-12-05 17:25:02
-->
<template>
<div class="table-container" ref="containerRef">
<VxeTable
border
ref="tableRef"
:data="tableData"
:height="tableHeight"
:column-config="{resizable: true}"
:row-config="{isHover: true}"
:virtual-y-config="{enabled: true, gt: 50}"
@checkbox-all="handleSelectionChange"
@checkbox-change="handleSelectionChange"
:render-format="{ autoMerge: false }"
>
<vxe-column type="checkbox" width="50" fixed="left" />
<vxe-column type="seq" width="70" />
<vxe-column
v-for="item in tableColumns"
sortable
:key="item.dataKey"
:field="item.dataKey"
:title="item.title"
:min-width="item.width"
:filters="item.fileList"
/>
</VxeTable>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
tableData: {
type: Array,
default: () => [],
},
tableColumns: {
type: Array,
default: () => [],
},
})
const containerRef = ref<HTMLDivElement | null>(null)
const tableHeight = ref(0)
onMounted(async () => {
await nextTick()
setTableHeight()
})
const setTableHeight = () => {
if (!containerRef.value) return
const containerHeight = containerRef.value.clientHeight
tableHeight.value = containerHeight
}
window.addEventListener('resize', () => {
setTableHeight()
})
onMounted(() => {
if (!containerRef.value) return
const resizeObserver = new ResizeObserver(() => {
setTableHeight()
})
resizeObserver.observe(containerRef.value)
})
// 复选框选中
const handleSelectionChange = (params: any) => {
emits('selection-change', params.records)
}
const emits = defineEmits(['selection-change'])
</script>
<style scoped lang="scss">
.table-container {
height: calc(100vh - 210px);
:deep(.vxe-table--column) {
font-weight: normal;
color: #636369;
}
:deep(.col--filter) {
.vxe-cell--filter {
float: right;
vertical-align: middle;
margin-top: 3px;
}
.vxe-filter--btn::before {
content: '' !important;
display: inline-block;
width: 16px;
height: 16px;
background: url('@/assets/svgs/doc/filter.svg') no-repeat center center;
background-size: 100% 100%;
vertical-align: middle;
}
.vxe-cell--title {
color: #636369;
}
}
:deep(.is--filter-active) {
.vxe-cell--filter {
float: right;
vertical-align: middle;
margin-top: 3px;
}
.vxe-filter--btn::before {
content: '' !important;
display: inline-block;
width: 16px;
height: 16px;
background: url('@/assets/svgs/doc/filter-checked.svg') no-repeat center center;
background-size: 100% 100%; // 适配图片尺寸
vertical-align: middle;
}
.vxe-cell--title {
color: #005aae;
}
}
}
</style>