Vue3 组合式 API 下的大列表渲染优化:从 3.2s 到 180ms 的实践记录|2026-11-02
在做后台管理系统的过程中,遇到一个典型的性能问题:一个包含 5000 条数据的表格页面,首屏渲染耗时 3.2 秒,滚动时明显掉帧。这篇文章记录完整的排查过程和优化方案,希望对做类似场景的同学有帮助。
问题现象
页面结构很简单,一个表格组件 + 分页 + 筛选栏。数据量 5000 条(后端一次性返回,产品要求"全量可选",不能分页)。
首次打开时:
- 白屏约 1.4s
- 渲染完成到可交互约 3.2s
- 滚动时 FPS 掉到 20 左右
用 Chrome Performance 面板录制后发现,主线程被大量 createElement 和 patch 调用占满,Vue 的 reactive 代理开销占比约 38%。
定位问题
1. 数据本身被深层响应式代理
Vue3 的 reactive 默认是深层代理。5000 条数据、每条 20 个字段,意味着创建了 10 万个 Proxy 对象。
// 问题代码
const state = reactive({
list: [], // 直接塞 5000 条
})
state.list = await fetchAll() // 触发深层代理
Proxy 创建本身有成本,更麻烦的是每次访问属性都会走 get 拦截器。
2. 表格单元格内的格式化函数在模板里执行
<template>
<el-table :data="list">
<el-table-column
v-for="col in columns"
:key="col.prop"
:label="col.label"
:formatter="(row) => formatCell(row, col)" <!-- 每行每列都新建函数 -->
/>
</el-table>
</template>
formatter 每次渲染都会创建新函数,导致子组件 props 变化,无法命中缓存。
3. 一次性渲染全部行
没有虚拟滚动,DOM 节点数量达到 5000 × 8 = 4 万个。
优化方案
方案一:用 shallowRef 承载大列表
列表数据只需要整体替换,不需要深层响应,用 shallowRef 可以跳过深层代理:
import { shallowRef, triggerRef } from 'vue'
const list = shallowRef([])
async function load() {
const raw = await fetchAll()
list.value = raw // 只代理数组本身,内部对象不代理
}
// 需要局部修改时手动触发更新
function updateRow(index, patch) {
Object.assign(list.value[index], patch)
triggerRef(list) // 显式通知
}
这一项直接把初始化时间从 1.4s 降到 0.5s。
方案二:formatter 提到 setup 外,用 Map 缓存
const formatterMap = new Map()
function getFormatter(col) {
if (!formatterMap.has(col.prop)) {
formatterMap.set(col.prop, (row) => formatCell(row, col))
}
return formatterMap.get(col.prop)
}
<el-table-column
v-for="col in columns"
:key="col.prop"
:formatter="getFormatter(col)"
/>
方案三:虚拟滚动
换用 el-table-v2(Element Plus 的虚拟化表格),只渲染可视区域内的行:
<template>
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:columns="columns"
:data="list"
:width="width"
:height="height"
:row-height="48"
fixed
/>
</template>
</el-auto-resizer>
</template>
DOM 节点从 4 万降到约 600。
方案四:把筛选计算移出渲染路径
原来筛选做在 computed 里,输入框每敲一个字就重算 5000 条:
// 优化前
const filtered = computed(() => list.value.filter(match))
改成防抖 + 手动触发:
import { ref, shallowRef } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const keyword = ref('')
const filtered = shallowRef([])
const applyFilter = useDebounceFn(() => {
const k = keyword.value.trim().toLowerCase()
filtered.value = k
? list.value.filter(r => r.name.toLowerCase().includes(k))
: list.value
}, 200)
最终结果
指标 | 优化前 | 优化后
首屏可交互 | 3.2s | 0.18s
DOM 节点数 | 40000+ | 约 600
滚动 FPS | 20 | 58
内存占用 | 186MB | 74MB
踩过的坑
1. shallowRef 配合 v-model 会失效。 表格里如果有可编辑单元格,v-model 写回时不会触发视图更新,必须手动 triggerRef。这种情况建议对需要编辑的子结构单独用 reactive。
2. el-table-v2 的列宽必须显式给。 不给 width 会算出 0,整个表格空白,且控制台不报错,很容易卡住。
3. 虚拟滚动下不要用 row-key 做复杂计算。 每帧都会调用,函数体太重同样会掉帧。
4. 不要把 columns 定义在模板里。 每次渲染都新建数组,虚拟表格会整体重排。
小结
Vue3 的响应式很强,但对大数据量场景要主动"降级":能用 shallowRef 就不用 reactive,能虚拟化就不要全量渲染,能在渲染外算的就不放模板里算。这三条基本能覆盖大部分后台表格的性能问题。
如果你的列表还需要排序、拖拽、行内编辑,建议再往上加一层数据分片(chunk),把 5000 条切成 200 条一组,配合 requestIdleCallback 分批渲染,首屏体验还能再提升一个档次。
- 点赞
- 收藏
- 关注作者
评论(0)