Vue3 组合式 API 下的大列表渲染优化:从 3.2s 到 180ms 的实践记录|2026-11-05

举报
yd_271851850 发表于 2026/11/05 11:44:26 2026/11/05
【摘要】 在做后台管理系统的过程中,遇到一个典型的性能问题:一个包含 5000 条数据的表格页面,首屏渲染耗时 3.2 秒,滚动时明显掉帧。这篇文章记录完整的排查过程和优化方案,希望对做类似场景的同学有帮助。 问题现象 页面结构很简单,一个表格组件 + 分页 + 筛选栏。数据量 5000 条(后端一次性返回,

在做后台管理系统的过程中,遇到一个典型的性能问题:一个包含 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 分批渲染,首屏体验还能再提升一个档次。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。