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
关键特性
-
自动依赖追踪
会检测
<AsyncTable>
的所有异步操作,包括:- 组件本身的异步加载(
defineAsyncComponent
) - 组件内部可能存在的
async setup()
- 组件树中的其他异步依赖
- 组件本身的异步加载(
-
异步完成时机:
- 异步组件文件(
HeavyTable.vue
)的 JS/CSS 资源加载完成 - 组件本身(包括子组件)的异步
setup()
执行完成
- 异步组件文件(
-
统一加载状态
即使
<AsyncTable>
内部嵌套了更多异步组件,也会等待所有内容就绪后才整体显示,否则则显示 fallback slot 里的内容,这和 vue2 x项目常使用的 v-loading 类型,相比 v-loading 有以下优势:- 自动处理嵌套异步依赖
- 无需手动维护 loading 状态
- 更清晰的代码结构
-
错误传播
若加载失败,错误会冒泡到最近的
<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>
四、内存泄漏防治
常见陷阱分析
- 动态组件残留
在 Vue 中,当使用v-show
或v-if
切换组件时,如果组件内部有定时器、事件监听或第三方库实例等资源,仅仅隐藏组件(v-show
)不会释放这些资源,必须通过销毁组件(v-if
)来彻底释放内存。 - 全局事件残留
在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>
关键要点:
-
添加和移除必须使用同一个函数引用
-
适用于以下场景:
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>
总结:任何手动创建的资源,必须手动销毁。