Vue 3 + Element Plus 可编辑表格性能优化

52 阅读4分钟

在后台管理系统中,可编辑表格几乎是刚需。

目前公司项目基于 Element Plus,实现方式也比较直接:在 el-table 的每一个单元格中直接嵌入 el-input,开发起来顺手,改动也非常直观。

但问题也来得很快。

当列数一多(比如 50+ 列),或者数据行稍微大一点,页面直接开始卡顿,输入延迟、滚动掉帧,严重的时候浏览器都直接卡死。

最近正好踩了这个坑,花了一点时间把性能问题系统性地优化了一下,效果还挺明显,这里记录一下思路和实现。


问题本质:组件实例太多了

先说结论:

不是 el-table 慢,而是 el-input 太重。

el-input 本身是一个复杂组件:

  • 响应式
  • 校验
  • 事件监听
  • 样式计算
  • ……

如果你有:

  • 50 列 × 20 行 = 1000 个 el-input
  • Vue 需要同时维护 1000 个组件实例

,一个el-input实例不只是一个input,而是多层div等节点嵌套,数量一多,dom节点直接爆炸,CPU 和内存当场起飞,卡顿是必然的。


核心优化思路(两句话版)

我最后的优化方案,其实就两点:

  1. 读写分离
    平时只展示文本,点谁才渲染谁的 el-input
  2. 分批渲染
    列数太多,不一次性渲染完,分几帧慢慢上屏

下面展开说。


一、性能提升最大的一步:点击才编辑(EditCell)

这是最关键、收益也最大的一步。 解决思路很简单:

平时只展示纯文本,点哪儿,哪儿才变成输入框。

我们封装一个 EditCell 组件,它主要负责两个状态的切换:

  1. 非编辑状态(View Mode) : 渲染一个普通的 divspan。这属于原生的 HTML 标签,渲染成本几乎可以忽略不计。
  2. 编辑状态(Edit Mode) : 只有当用户点击这个单元格时,才把它替换为 el-input,并自动让它获得焦点。当用户点到别处(失焦 blur)时,立刻销毁 Input,变回文本。

这样一来:

无论表格有多大,页面上同时存在的 el-input,通常只有 1 个。


EditCell 组件实现

Template

<template>
  <div v-if="isEditing" class="edit-cell-input">
    <el-input
      ref="inputRef"
      v-model="internalValue"
      @blur="handleBlur"
    />
  </div>

  <div v-else class="edit-cell-display" @click="handleEdit">
    <span v-if="internalValue">{{ internalValue }}</span>
    <span v-else class="placeholder">请输入</span>
  </div>
</template>

Script

核心逻辑代码(EditCell.vue):

const handleCellClick = () => {
  // 1. 切换状态,准备渲染 el-input
  isEditing.value = true;
  
  // 2. 等待 DOM 更新完成后,立即让 input 获取焦点
  nextTick(() => {
    // inputRef 是 el-input 组件的引用
    inputRef.value?.focus();
  });
};

const onBlur = () => {
  // 3. 失焦后立即切回文本模式,销毁 input 组件,释放内存
  isEditing.value = false;
  emit('change', internalValue.value);
};

通过这一招,原本页面上几千个 el-input 瞬间减少到了 0 个(或者是 1 个,如果你正在编辑的话)。内存占用直线下降。

Style(关键在“看起来像 el-input”)

.edit-cell-display {
  width: 100%;
  height: 32px;
  box-sizing: border-box;
  text-align: left;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0 10px;
  color: #333;
  cursor: pointer;
  box-shadow: 0 0 0 1px var(--el-input-border-color, #dcdfe6) inset;
  border-radius: 4px;
  transition: box-shadow 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  &:hover {
    box-shadow: 0 0 0 1px var(--el-input-hover-border-color, #c0c4cc) inset;
  }
  span {
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

为了让切换过程不“跳”,我在查看模式下用 CSS 把 div 伪装成 el-input 的样子(高度、padding、圆角都一致)。这样,无论怎么切换,肉眼几乎看不出 DOM 的替换过程

这一点优化完,输入卡顿基本就消失了。而且 EditCell 被封装成独立组件后,后续在其他表格或表单场景中也可以直接复用,一次优化,长期受益。


二、辅助优化:列太多怎么办?分帧渲染

解决了「组件太重」的问题后,还有一个场景:

列真的非常多,比如 80、90 列。

哪怕每个单元格只是普通 DOM,
90 列 × 20 行 = 1800 个 td,一次性让浏览器在 DOM 树中插入 96 列 x 30 行的节点,主线程依然会阻塞,导致页面出现短暂的“白屏”或无法响应。。

解决思路:时间分片(Time Slicing)

不要一次性把所有列都丢给 el-table,而是:

  • 每一帧只渲染一小部分列(比如 10 列)
  • requestAnimationFrame 分几帧完成
  • 用户几乎感知不到,但主线程不会被一次性堵死

核心代码

const addColumnsTimeSliced = () => {
  let currentIndex = 0;
  const batchSize = 10; // 每帧只处理 10 列

  const doBatch = () => {
    // 1. 计算这一批次的结束点
    const endIndex = Math.min(currentIndex + batchSize, totalColumns);
    
    // 2. 截取这一批列配置,加入到当前显示的列列表中
    // 使用 markRaw 进一步告诉 Vue:这些列配置不需要深度监听,省点力气
    const nextBatch = allColumns.slice(currentIndex, endIndex).map(col => markRaw(col));
    internalColumns.value = [...internalColumns.value, ...nextBatch];
    
    currentIndex = endIndex;

    // 3. 如果还没渲染完,就预约下一帧继续
    if (currentIndex < totalColumns) {
      requestAnimationFrame(doBatch);
    }
  };

  // 启动任务
  requestAnimationFrame(doBatch);
};

总结一下

这次优化后,表格从「卡到没法用」变成了「基本无卡顿流畅输入」。

核心就两点:

  • 减负
    用 EditCell,把 el-input 从“全量渲染”变成“按需出现”
  • 分流
    requestAnimationFrame 把大渲染任务拆开,避免主线程阻塞