Element Plus性能优化实战:从卡顿到流畅的进阶指南

Element Plus 作为 Vue 3 生态中最受欢迎的 UI 组件库之一,在中后台项目中广泛应用。但随着项目规模扩大,组件滥用、渲染卡顿等问题逐渐暴露。本文将分享 4 个核心优化策略,结合代码示例,助你打造高效的企业级应用。


一、按需加载:减少打包体积

按需加载不仅能减少打包后的体积,也可以减少 Vue 加载过多不必要的组件。达到优化性能的作用。

1.1 基本配置

bash 复制代码
npm install element-plus @element-plus/icons-vue  # 核心库和图标
npm install unplugin-element-plus unplugin-auto-import unplugin-vue-components -D  # 按需导入插件

1.2. 配置 vite.config.js

javascript 复制代码
import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    vue(),
    // 自动导入 Element Plus 组件和样式
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver(),
      ],
    }),
  ],
});

1.3. 我们先把组件注册注释掉

javascript 复制代码
// main.ts
import { createApp } from "vue";
// import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";

const app = createApp(App);

// app.use(ElementPlus);
app.mount("#app");

1.4. 组件使用(无需手动导入)

xml 复制代码
<template>
  <el-button type="primary">按钮</el-button>
  <!-- 直接使用,无需import -->
</template>

1.5 优化效果

这是优化前的打包体积:

这是优化后的打包体积:

可以看到包的体积大小有明显减少,如果项目里并没有用到全部的 Element 组件,推荐使用按需导入减少包体积。

那么为什么可以减少体积呢?为什么可以起到优化性能的作用呢?

如果是全量导入的话,所有组件及其样式会在项目构建阶段(build time)被完整打包到最终产物中。当应用启动时,所有组件一次性注册到 Vue 的全局组件系统中。如果你是按需导入,会在需要用到组件的地方再加载进来:

可以看到当使用按需加载的时候,会在使用到组件的时候再去加载该组件,那么肯定你会有问题了,每次用到都加载一次吗?

那肯定不是,element 没那么蠢,具体的加载时机如下👇

按需加载的触发时机

  • 首次使用组件时加载:当组件第一次被渲染到页面时,浏览器会发起网络请求加载对应的代码块(chunk)。
  • 后续使用 :若组件代码已被缓存,则直接读取缓存,不再重复加载

二、组件懒加载:提升首屏速度

问题场景

复杂组件(如 ElTable 渲染千行数据)会阻塞主线程。

优化方案

结合 Vue 3 的 defineAsyncComponent<Suspense> 实现懒加载。

2.1. defineAsyncComponent

作用:定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。

典型场景

  • 按需加载体积较大的组件
  • 路由级代码分割
  • 条件加载第三方组件库

主要特性

javascript 复制代码
const AsyncTable = defineAsyncComponent(() =>
  import("./components/HeavyTable.vue")
);

这样就是声明了一个异步组件,它的核心作用是将 AdminPageComponent.vue 从主包中分离,生成独立的 chunk 文件,在你需要用的时候再通过 Webpack/Vite 的 import() 动态导入语法导入。这样就可以起到一个优化首屏渲染速度的效果。

bash 复制代码
dist/
├─ js/
│  ├─ main.js        # 主包(初始加载)
│  └─ HeavyTable.js   # 按需加载的独立 chunk

2.2. Suspense

关键特性

  1. 自动依赖追踪

    会检测 <AsyncTable> 的所有异步操作,包括:

    • 组件本身的异步加载(defineAsyncComponent
    • 组件内部可能存在的 async setup()
    • 组件树中的其他异步依赖
  2. 异步完成时机:

    • 异步组件文件(HeavyTable.vue)的 JS/CSS 资源加载完成
    • 组件本身(包括子组件)的异步 setup() 执行完成
  3. 统一加载状态

    即使 <AsyncTable> 内部嵌套了更多异步组件,也会等待所有内容就绪后才整体显示,否则则显示 fallback slot 里的内容,这和 vue2 x项目常使用的 v-loading 类型,相比 v-loading 有以下优势:

    • 自动处理嵌套异步依赖
    • 无需手动维护 loading 状态
    • 更清晰的代码结构
  4. 错误传播

    若加载失败,错误会冒泡到最近的 <error-boundary>(需要单独配置)

xml 复制代码
// 父组件
<template>
  <Suspense>
    <template #default>
      <AsyncTable :data="largeData" />
    </template>
    <template #fallback>
      <el-skeleton :rows="5" animated />
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from "vue";

const AsyncTable = defineAsyncComponent(() =>
  import("./components/HeavyTable.vue")
);

// 生成模拟大数据
const generateLargeData = (count = 1000) => {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
    age: Math.floor(Math.random() * 30 + 20),
    email: `user${i + 1}@example.com`,
    company: ["阿里巴巴", "字节跳动", "蟹堡王"][i % 3],
    department: ["技术部", "市场部", "摸鱼部"][i % 3],
    position: ["工程师", "经理", "摆烂的"][i % 3],
    salary: Math.floor(Math.random() * 50000 + 50000),
  }));
};

const largeData = generateLargeData(1000); // 生成1000条测试数据
</script>

在这个例子中,Suspense 会判定你异步结束,加载时会触发 <Suspense> 的 fallback 状态,实际项目中一般都是后端网络请求回来的数据,可以看下面这个例子:

ini 复制代码
<template>
  <el-table
    :data="internalData"
    border
    style="width: 100%; margin: 20px 0"
    height="600px"
    row-key="id"
  >
    <el-table-column type="index" label="序号" width="60" fixed />
    <el-table-column prop="name" label="姓名" width="120" fixed />
    <el-table-column prop="age" label="年龄" width="100" sortable />
    <el-table-column prop="email" label="邮箱" width="240" />
    <el-table-column prop="company" label="公司" width="200" />
    <el-table-column prop="department" label="部门" width="150" />
    <el-table-column prop="position" label="职位" width="180" />
    <el-table-column prop="salary" label="薪资" width="120" sortable />
  </el-table>
</template>

<script setup>
import { ref } from "vue";
import axios from "axios";

// 异步获取数据(会被 Suspense 捕获)
const internalData = ref([]);
const fetchData = async () => {
  const res = await axios.get("/api/data"); // 模拟异步请求
  internalData.value = res.data;
};

await fetchData(); // 关键:在 setup 中使用 await
</script>

<style scoped>
.el-table {
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
</style>

异步请求的话,只需要在顶层使用 await 就可以被捕获到,这样就可以实现首屏视觉以及速度的一个整体优化。

⚠️ 注意:Suspense 目前是实验性特性,API 可能在未来版本中变动。

三、表格性能优化:万级数据渲染

问题场景

渲染海量数据时出现滚动卡顿。

解决方案

1. 虚拟滚动

虚拟列表是常见的一种优化页面的方式,它的本质就是通过 JS 计算让页面始终只加载一定数量的 DOM,防止加载态度页面炸了。

ini 复制代码
  <el-table-v2
    :columns="columns"
    :data="data"
    :width="700"
    :height="400"
    fixed
  />

element 自带有一个 table 的虚拟列表,也可以用第三方库,推荐:vue3-virtual-scroll-list

2. 分页

常见的是使用后端分页,前端也可以通过计算属性实现分页:

xml 复制代码
<template>
  <div class="pagination-demo">
    <!-- 数据表格 -->
    <el-table :data="paginatedData" border style="width: 100%">
      <el-table-column prop="id" label="ID" width="80" />
      <el-table-column prop="name" label="Name" />
    </el-table>

    <!-- 分页控件 -->
    <el-pagination
      background
      layout="total, sizes, prev, pager, next, jumper"
      :total="fullData.length"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="[10, 20, 50]"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script setup>
import { ref, computed } from "vue";

// 全量测试数据
const fullData = ref(
  Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
  }))
);

// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(10);

// 计算当前页数据
const paginatedData = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value;
  const end = start + pageSize.value;
  return fullData.value.slice(start, end);
});

// 处理分页大小变化
const handleSizeChange = (newSize) => {
  pageSize.value = newSize;
  // 重置到第一页以避免超出范围
  currentPage.value = 1;
};

// 处理页码变化
const handleCurrentChange = (newPage) => {
  currentPage.value = newPage;
};
</script>

<style scoped>
.pagination-demo {
  padding: 20px;
}

.el-pagination {
  margin-top: 20px;
  justify-content: flex-end;
}
</style>

四、内存泄漏防治

常见陷阱分析

  1. 动态组件残留
    在 Vue 中,当使用 v-showv-if 切换组件时,如果组件内部有定时器、事件监听或第三方库实例等资源,仅仅隐藏组件(v-show)不会释放这些资源,必须通过销毁组件(v-if)来彻底释放内存。
  2. 全局事件残留
    window/document 上添加的事件监听器不会随组件销毁自动移除,如果不手动清理,每次组件创建都会叠加新的监听器,导致内存持续增长。

解决方案详解

一、强制销毁动态组件(弹窗/表单案例)

xml 复制代码
<template>
  <!-- 通过 key 强制重新创建对话框 -->
  <el-dialog v-model="visible" :key="dialogKey">
    <FormComponent @submit="handleSubmit"/>
  </el-dialog>
</template>

<script setup lang="ts">
const visible = ref(false);
const dialogKey = ref(0);

// 关闭时修改 key 值
const closeDialog = () => {
  visible.value = false;
  dialogKey.value++; // key 变化会触发 Vue 销毁旧组件实例
};
</script>

原理说明

  • 修改 key 会导致 Vue 销毁旧组件并重新创建新组件实例
  • 彻底释放旧组件内的定时器、事件监听等资源
  • 适合富表单、带图表的弹窗等重型组件

二、手动清理事件监听(窗口缩放案例)

xml 复制代码
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';

// 必须保持函数引用一致
const handleResize = () => {
  console.log(window.innerWidth);
};

onMounted(() => {
  window.addEventListener('resize', handleResize);
});

// 卸载前必须清理
onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize);
});
</script>

关键要点

  1. 添加和移除必须使用同一个函数引用

  2. 适用于以下场景:

    • window/document 事件
    • 第三方库事件(如地图SDK)
    • WebSocket 监听

扩展场景处理

第三方库实例清理(以 ECharts 为例)

xml 复制代码
<script setup>
import * as echarts from 'echarts';

let chartInstance: echarts.ECharts | null = null;

onMounted(() => {
  chartInstance = echarts.init(document.getElementById('chart'));
  chartInstance.setOption({ /* ... */ });
});

onBeforeUnmount(() => {
  // 必须显式销毁图表实例
  chartInstance?.dispose();
  chartInstance = null;
});
</script>

定时器清理

xml 复制代码
<script setup>
let timer: number | null = null;

onMounted(() => {
  timer = setInterval(() => {
    console.log('心跳');
  }, 1000);
});

onBeforeUnmount(() => {
  timer && clearInterval(timer);
});
</script>

总结:任何手动创建的资源,必须手动销毁。

相关推荐
Perfect—完美12 分钟前
Vue 3 事件总线详解:构建组件间高效通信的桥梁
前端·javascript·vue.js
二川bro22 分钟前
模拟类似 DeepSeek 的对话
前端·人工智能
祈澈菇凉1 小时前
Vue 中如何实现自定义指令?
前端·javascript·vue.js
sorryhc1 小时前
解读Ant Design X API流式响应和流式渲染的原理
前端·react.js·ai 编程
1024小神1 小时前
vue/react前端项目打包的时候加上时间,防止后端扯皮
前端·vue.js·react.js
拉不动的猪1 小时前
刷刷题35(uniapp中级实际项目问题-2)
前端·javascript·面试
bigcarp2 小时前
理解langchain langgraph 官方文档示例代码中的MemorySaver
java·前端·langchain
FreeCultureBoy2 小时前
从 VS Code 的插件市场下载扩展插件
前端
前端菜鸟日常2 小时前
Webpack 和 Vite 的主要区别
前端·webpack·node.js
仙魁XAN3 小时前
Flutter 学习之旅 之 flutter 在设备上进行 全面屏 设置/隐藏状态栏/隐藏导航栏 设置
前端·学习·flutter