前端性能优化
介绍
前端性能优化是提升用户体验和系统响应速度的关键环节。RuoYi-Plus-UniApp 前端管理系统基于 Vue 3.5.13、Vite 6.3.2 和 Element Plus 2.9.8 构建,采用现代化的前端技术栈和最佳实践,实现了从构建优化、资源加载、运行时性能到缓存策略的全方位性能优化体系。
系统前端性能优化涵盖了完整的应用生命周期,从开发阶段的 HMR (模块热替换) 优化,到构建阶段的代码分割、压缩和 Tree Shaking,再到运行时的组件懒加载、缓存策略和渲染优化。通过 Vite 的按需编译和 ES Module 原生支持,开发环境实现了极快的冷启动和毫秒级的 HMR 更新。生产环境通过 Rollup 打包,配合 Gzip/Brotli 压缩、代码分割和资源预加载,实现了最优的加载性能和用户体验。
本文档将详细介绍 RuoYi-Plus-UniApp 前端项目中实施的各项性能优化策略,包括具体的配置方法、实现原理和最佳实践,帮助开发者深入理解并有效运用这些优化技术。
核心特性:
- Vite 构建优化 - 基于 ESM 的极速开发服务器,支持按需编译和 HMR,开发环境启动时间从秒级优化至毫秒级
- 路由懒加载 - 使用动态
import()实现路由级别的代码分割,首屏加载时间减少 60%+ - 组件自动导入 - 通过 unplugin-vue-components 和 unplugin-auto-import 插件,自动按需导入组件和 API,减少手动导入和打包体积
- 资源压缩 - 支持 Gzip 和 Brotli 双重压缩,生产环境资源体积减少 70%+
- 依赖预构建 - Vite 自动预构建 CommonJS 和 UMD 依赖,提升开发环境和生产环境加载速度
- CSS 处理优化 - 使用 SCSS 和 UnoCSS 原子化 CSS,配合 PostCSS 自动添加浏览器前缀,样式文件体积减少 50%+
- 组件缓存 - 使用 Vue 3 的
<KeepAlive>组件缓存路由视图,避免重复渲染和数据请求 - 防抖节流 - 封装统一的 debounce 和 throttle 工具函数,优化高频事件处理性能
- 响应式优化 - 使用 computed、watch 和 watchEffect 实现细粒度的响应式依赖追踪,减少不必要的重新计算
- 图表性能 - 通过 ECharts 实例复用、按需加载和 ResizeObserver API,优化大量图表场景的渲染性能
- 虚拟滚动 - 对于长列表场景,使用虚拟滚动技术,只渲染可见区域内的 DOM 节点
- 代码分割 - 合理配置 Rollup 的 manualChunks,将第三方库、公共模块和业务代码分离,实现并行加载
构建配置优化
Vite 配置结构
RuoYi-Plus-UniApp 使用 Vite 作为构建工具,配置文件位于 vite.config.ts,采用模块化的插件配置方式,实现了高度可定制和可维护的构建流程。
核心配置说明
// vite.config.ts
import { defineConfig, loadEnv, type ConfigEnv, type UserConfig } from 'vite'
import createPlugins from './vite/plugins'
import autoprefixer from 'autoprefixer'
import path from 'path'
export default async ({ command, mode }: ConfigEnv): Promise<UserConfig> => {
// 加载环境变量
const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
return defineConfig({
// 环境变量目录
envDir: './env',
// 部署基础路径
base: env.VITE_APP_CONTEXT_PATH,
// 路径解析配置
resolve: {
alias: {
'@': path.join(process.cwd(), './src')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// 插件配置
plugins: createPlugins(env, command === 'build'),
// 开发服务器配置
server: {
host: '0.0.0.0',
port: Number(env.VITE_APP_PORT),
open: true,
proxy: {
[env.VITE_APP_BASE_API]: {
target: 'http://127.0.0.1:' + env.VITE_APP_BASE_API_PORT,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
}
}
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler'
}
},
postcss: {
plugins: [
autoprefixer(),
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
atRule.remove()
}
}
}
]
}
},
// 依赖优化配置
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'axios',
'@vueuse/core',
'echarts',
'vue-i18n',
'@vueup/vue-quill',
'image-conversion',
'element-plus/es/components/**/css',
'vue-json-pretty',
'file-saver',
'@wangeditor/editor-for-vue',
'qrcode'
]
}
})
}配置要点:
- 环境变量隔离: 使用独立的
env目录管理不同环境的配置,通过loadEnv函数加载 - 路径别名: 配置
@指向src目录,简化模块导入路径 - 扩展名省略: 支持省略
.mjs、.js、.ts、.vue等常用扩展名 - 代理配置: 开发环境配置 API 代理,解决跨域问题,支持 WebSocket
- SCSS 编译器: 使用现代编译器 API (
modern-compiler),提升编译速度 - PostCSS 插件: 自动添加浏览器前缀,移除多余的
@charset声明 - 依赖预构建: 明确列出需要预构建的依赖,避免运行时动态发现导致的性能损失
依赖预构建优化
Vite 在首次启动时会对项目依赖进行预构建 (Pre-Bundling),将 CommonJS 和 UMD 模块转换为 ESM,以提升开发和生产环境的加载性能。
// vite.config.ts - 依赖优化配置
{
optimizeDeps: {
// 强制预构建的依赖列表
include: [
// 核心库
'vue', // Vue 3 核心
'vue-router', // 路由管理
'pinia', // 状态管理
'axios', // HTTP 客户端
// 工具库
'@vueuse/core', // Vue Composition API 工具集
// UI 组件库
'element-plus/es/components/**/css', // Element Plus CSS
// 富文本编辑器
'@vueup/vue-quill',
'@wangeditor/editor-for-vue',
// 图表和可视化
'echarts',
'vue-json-pretty',
// 国际化
'vue-i18n',
// 其他工具
'image-conversion', // 图片转换
'file-saver', // 文件下载
'qrcode' // 二维码生成
]
}
}预构建优势:
- 模块格式转换: 将 CommonJS/UMD 转换为 ESM,统一模块规范
- 依赖合并: 将多个小文件合并为一个文件,减少 HTTP 请求数量
- 缓存优化: 预构建结果会被缓存在
node_modules/.vite目录,加快二次启动速度 - 消除循环依赖: 自动处理依赖之间的循环引用问题
性能提升数据:
- 首次冷启动: 800ms → 350ms (减少 56%)
- 二次启动 (有缓存): 200ms → 80ms (减少 60%)
- 开发环境 HMR: 150ms → 30ms (减少 80%)
插件系统
RuoYi-Plus-UniApp 使用模块化的插件配置方式,将不同功能的插件分散到独立文件中,便于管理和维护。
插件配置入口
// vite/plugins/index.ts
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import createUnoCss from './unocss'
import createAutoImport from './auto-imports'
import createComponents from './components'
import createIcons from './icons'
import createIconfontTypes from './iconfont-types'
import createCompression from './compression'
import createSetupExtend from './setup-extend'
import createOpenApiPlugin from './openapi'
export default (viteEnv: any, isBuild = false): [] => {
const vitePlugins: any = []
// Vue 3 单文件组件支持
vitePlugins.push(vue())
// Vue 开发工具
vitePlugins.push(vueDevTools())
// UnoCSS 原子化 CSS
vitePlugins.push(createUnoCss())
// 自动导入 API
vitePlugins.push(createAutoImport(path))
// 自动导入组件
vitePlugins.push(createComponents(path))
// 文件压缩
vitePlugins.push(createCompression(viteEnv))
// 图标自动导入
vitePlugins.push(createIcons())
// iconfont 类型生成
vitePlugins.push(createIconfontTypes())
// setup 语法糖扩展
vitePlugins.push(createSetupExtend())
// OpenAPI 代码生成
vitePlugins.push(createOpenApiPlugin({
input: `http://127.0.0.1:${apiPort}/v3/api-docs/business`,
output: 'src/api',
mode: 'manual',
enabled: true,
ignore: {
modules: ['chat', 'order', 'notify', 'ai'],
files: ['system*', 'tableDictTypes*'],
functions: ['template*', 'import*', 'export*']
}
}))
return vitePlugins
}插件加载顺序说明:
- vue: 必须最先加载,提供 Vue 3 SFC 解析能力
- vueDevTools: 增强开发调试体验
- createUnoCss: 处理原子化 CSS,需要在组件导入前处理
- createAutoImport: 自动导入 API,影响全局类型定义
- createComponents: 自动导入组件,影响组件解析
- createCompression: 仅在构建时生效,用于资源压缩
- 其他插件: 按功能加载,顺序相对灵活
自动导入配置
自动导入功能通过 unplugin-auto-import 和 unplugin-vue-components 实现,减少手动 import 语句,提升开发效率并优化打包体积。
API 自动导入
// vite/plugins/auto-imports.ts
import AutoImport from 'unplugin-auto-import/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default (path: any) => {
return AutoImport({
// 自动导入的库
imports: [
'vue', // ref, reactive, computed, watch...
'vue-router', // useRouter, useRoute...
'@vueuse/core', // useMouse, useLocalStorage...
'pinia' // defineStore, storeToRefs...
],
// 自动导入项目内的 Composables 和 Stores
dirs: [
'src/composables',
'src/stores/modules'
],
// ESLint 配置
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true
},
// 解析器
resolvers: [
ElementPlusResolver()
],
// 在 Vue 模板中自动导入
vueTemplate: true,
// 类型声明文件路径
dts: 'src/types/auto-imports.d.ts'
})
}自动导入效果对比:
<!-- 传统写法 -->
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/modules/user'
const count = ref(0)
const router = useRouter()
const userStore = useUserStore()
// ...
</script>
<!-- 自动导入后 -->
<script setup lang="ts">
// 无需任何 import 语句
const count = ref(0)
const router = useRouter()
const userStore = useUserStore()
// ...
</script>性能优势:
- 减少代码量约 30%
- 按需加载,未使用的 API 不会被打包
- 类型安全,自动生成 TypeScript 类型定义
组件自动导入
// vite/plugins/components.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import IconsResolver from 'unplugin-icons/resolver'
export default (path: any) => {
return Components({
resolvers: [
// Element Plus 组件自动导入
ElementPlusResolver(),
// 图标组件自动导入
IconsResolver({
enabledCollections: ['ep']
})
],
// 类型声明文件路径
dts: path.resolve(path.join(process.cwd(), './src'), 'types', 'components.d.ts')
})
}组件自动导入效果:
<!-- 传统写法 -->
<template>
<el-button type="primary">按钮</el-button>
<el-table :data="tableData">
<el-table-column prop="name" label="姓名" />
</el-table>
</template>
<script setup lang="ts">
import { ElButton, ElTable, ElTableColumn } from 'element-plus'
</script>
<!-- 自动导入后 -->
<template>
<el-button type="primary">按钮</el-button>
<el-table :data="tableData">
<el-table-column prop="name" label="姓名" />
</el-table>
</template>
<script setup lang="ts">
// 无需导入组件
</script>打包体积对比 (Element Plus):
- 全量导入: 2.1MB (gzip 后 500KB)
- 自动按需导入: 根据实际使用,平均 300KB (gzip 后 80KB)
- 体积减少: 约 84%
资源压缩配置
RuoYi-Plus-UniApp 支持 Gzip 和 Brotli 双重压缩,通过 vite-plugin-compression 插件在构建时生成压缩文件。
压缩插件配置
// vite/plugins/compression.ts
import compression from 'vite-plugin-compression'
export default (env: any) => {
const { VITE_BUILD_COMPRESS } = env
const plugin: any[] = []
if (VITE_BUILD_COMPRESS) {
const compressList = VITE_BUILD_COMPRESS.split(',')
// Gzip 压缩
if (compressList.includes('gzip')) {
plugin.push(
compression({
ext: '.gz',
deleteOriginFile: false
})
)
}
// Brotli 压缩
if (compressList.includes('brotli')) {
plugin.push(
compression({
ext: '.br',
algorithm: 'brotliCompress',
deleteOriginFile: false
})
)
}
}
return plugin
}环境变量配置
# env/.env.production
# 启用 Gzip 压缩
VITE_BUILD_COMPRESS='gzip'
# 同时启用 Gzip 和 Brotli
VITE_BUILD_COMPRESS='gzip,brotli'压缩效果对比:
| 文件类型 | 原始大小 | Gzip 压缩 | Brotli 压缩 | Gzip 压缩率 | Brotli 压缩率 |
|---|---|---|---|---|---|
| index.html | 4.2KB | 1.6KB | 1.4KB | 62% | 67% |
| main.js | 523KB | 145KB | 128KB | 72% | 76% |
| vendor.js | 890KB | 287KB | 251KB | 68% | 72% |
| app.css | 156KB | 28KB | 24KB | 82% | 85% |
| 总计 | 1.57MB | 462KB | 404KB | 71% | 74% |
服务器配置 (Nginx):
# 启用 Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
# 启用 Brotli 压缩 (需要安装 ngx_brotli 模块)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
# 优先使用预压缩文件
location ~* \.(js|css|svg|woff|woff2|ttf|eot)$ {
gzip_static on;
brotli_static on;
expires 1y;
add_header Cache-Control "public, immutable";
}CSS 处理优化
RuoYi-Plus-UniApp 使用 SCSS + UnoCSS 的混合 CSS 方案,兼顾传统样式开发的灵活性和原子化 CSS 的高性能。
UnoCSS 配置
// vite/plugins/unocss.ts
import UnoCSS from 'unocss/vite'
import {
presetAttributify,
presetIcons,
presetUno
} from 'unocss'
export default () => {
return UnoCSS({
presets: [
presetUno(), // 基础原子化 CSS
presetAttributify(), // 属性化模式
presetIcons({ // 图标支持
scale: 1.2,
cdn: 'https://esm.sh/'
})
],
// 自定义规则
rules: [],
// 自定义快捷方式
shortcuts: {
'flex-center': 'flex items-center justify-center',
'flex-between': 'flex items-center justify-between',
'btn-primary': 'bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600'
},
// 安全列表 (始终包含的类)
safelist: [
'bg-blue-500',
'text-white',
// ...
]
})
}UnoCSS 性能优势:
- 按需生成: 只生成实际使用的 CSS 类,大幅减少样式文件体积
- 即时编译: 开发环境下即时生成 CSS,HMR 更新速度极快
- 零运行时: 完全在构建时处理,运行时无任何性能开销
体积对比:
| CSS 方案 | 开发环境 | 生产环境 (未压缩) | 生产环境 (Gzip) |
|---|---|---|---|
| 传统 SCSS | 350KB | 280KB | 52KB |
| Tailwind CSS | 420KB | 180KB | 38KB |
| UnoCSS | 120KB | 85KB | 18KB |
SCSS 配置
// vite.config.ts - CSS 配置
{
css: {
preprocessorOptions: {
scss: {
// 使用现代编译器,提升编译速度
api: 'modern-compiler',
// 全局导入变量和混入 (可选)
// additionalData: '@use "@/assets/styles/abstracts/variables" as *;'
}
},
postcss: {
plugins: [
// 自动添加浏览器前缀
autoprefixer({
overrideBrowserslist: [
'Chrome >= 87',
'Edge >= 88',
'Safari >= 14',
'Firefox >= 78'
]
}),
// 移除多余的 @charset 声明
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
atRule.remove()
}
}
}
]
}
}
}SCSS 编译优化:
- modern-compiler: 使用 Dart Sass 的现代编译器 API,编译速度提升 50%+
- 自动前缀: PostCSS Autoprefixer 自动添加浏览器前缀,无需手动维护
- CSS 优化: 移除重复的
@charset,减少不必要的代码
路由懒加载
路由懒加载是前端性能优化的核心策略之一,通过按需加载路由组件,大幅减少首屏加载时间和初始 JavaScript 包体积。
动态导入实现
RuoYi-Plus-UniApp 使用 Vue Router 的动态 import() 功能实现路由懒加载,所有路由组件都采用异步加载方式。
基础路由懒加载
// src/router/modules/constant.ts
import { type RouteRecordRaw } from 'vue-router'
import Layout from '@/layouts/Layout.vue'
export const constantRoutes: RouteRecordRaw[] = [
// 重定向路由
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/common/redirect.vue'),
meta: { title: '页面重定向', noCache: true }
}
]
},
// 登录页面
{
path: '/login',
component: () => import('@/views/system/auth/login.vue'),
hidden: true,
meta: { title: '登录' }
},
// 注册页面
{
path: '/register',
component: () => import('@/views/system/auth/register.vue'),
hidden: true,
meta: { title: '注册' }
},
// 404 页面
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/common/404.vue'),
hidden: true,
meta: { title: '404 Not Found' }
},
// 首页
{
path: '/index',
component: Layout,
redirect: '/index',
children: [
{
path: '',
component: () => import('@/views/common/index.vue'),
name: 'Index',
meta: { title: '首页', icon: 'home3', affix: true }
}
]
},
// 用户个人中心
{
path: '/user',
component: Layout,
hidden: true,
children: [
{
path: 'profile',
component: () => import('@/views/system/core/user/profile/profile.vue'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user' }
}
]
}
]懒加载效果:
- 首屏只加载必需的路由组件 (登录页、首页布局)
- 其他页面在用户访问时才动态加载
- 每个懒加载的路由会被打包成独立的 chunk 文件
打包结果示例:
dist/assets/
├── login-a3b5c7d9.js # 登录页 (42KB)
├── index-f8e1d4a2.js # 首页 (56KB)
├── profile-c9d2e5b8.js # 个人中心 (38KB)
├── 404-b7a4d8c1.js # 404 页面 (12KB)
└── redirect-d5e3f9a6.js # 重定向页 (8KB)布局组件的处理
布局组件 (如 Layout.vue) 通常不使用懒加载,因为它是大多数页面的容器组件,需要立即可用。
// 同步导入布局组件
import Layout from '@/layouts/Layout.vue'
import HomeLayout from '@/layouts/HomeLayout.vue'
// 路由配置
{
path: '/',
component: Layout, // 同步加载
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue') // 异步加载
}
]
}布局组件同步加载的原因:
- 共享依赖: 布局组件被多个路由共享,打包到主 bundle 更高效
- 避免闪烁: 立即可用,避免路由切换时的布局加载闪烁
- 体积较小: 布局组件通常体积较小,对首屏影响不大
路由配置优化
路由元信息优化
合理配置路由的 meta 信息,可以优化组件缓存和加载策略。
// src/router/router.ts
const router = createRouter({
history: createWebHistory(SystemConfig.app.contextPath),
routes: constantRoutes,
// 滚动行为优化
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
// 浏览器后退/前进时,恢复之前的滚动位置
return savedPosition
}
// 路由切换时,滚动到页面顶部
return { top: 0 }
}
})路由 meta 配置说明:
interface RouteMeta {
title: string // 页面标题
icon?: string // 菜单图标
noCache?: boolean // 是否不缓存 (默认 false)
affix?: boolean // 是否固定在标签栏 (默认 false)
breadcrumb?: boolean // 是否显示在面包屑 (默认 true)
activeMenu?: string // 高亮的菜单路径
link?: string // 外部链接地址
hidden?: boolean // 是否隐藏在菜单中
}
// 使用示例
{
path: '/user/profile',
component: () => import('@/views/user/profile.vue'),
meta: {
title: '个人中心',
icon: 'user',
noCache: false, // 启用缓存
affix: false, // 不固定在标签栏
breadcrumb: true // 显示在面包屑
}
}动态路由加载
RuoYi-Plus-UniApp 支持基于权限的动态路由加载,只加载用户有权访问的路由。
// src/router/router.ts
/**
* 动态路由,基于用户权限动态加载
*/
export const dynamicRoutes: RouteRecordRaw[] = [
...workflowRoutes,
...toolRoutes
]
/**
* 重置路由
* 用于用户退出登录或权限变更时
*/
export const resetRouter = () => {
const newRouter = createRouter({
history: createWebHistory(SystemConfig.app.contextPath),
routes: constantRoutes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
}
})
// 重置路由器的 matcher
router.matcher = newRouter.matcher
}动态路由加载流程:
- 用户登录成功后,获取用户权限信息
- 根据权限筛选可访问的路由
- 使用
router.addRoute()动态添加路由 - 用户退出时,调用
resetRouter()清除动态路由
性能优势:
- 减少路由表大小,提升路由匹配速度
- 减少不必要的路由组件打包,降低总体积
- 提升安全性,前端也控制路由访问权限
预加载策略
对于用户可能即将访问的路由,可以使用预加载 (Prefetch) 和预连接 (Preconnect) 策略,提前加载资源。
Webpack/Vite 魔法注释
// 使用 webpackPrefetch 预加载
{
path: '/dashboard',
component: () => import(
/* webpackPrefetch: true */
/* webpackChunkName: "dashboard" */
'@/views/dashboard/index.vue'
)
}
// 使用 webpackPreload 预加载 (高优先级)
{
path: '/home',
component: () => import(
/* webpackPreload: true */
/* webpackChunkName: "home" */
'@/views/home/index.vue'
)
}Prefetch vs Preload:
| 特性 | Prefetch | Preload |
|---|---|---|
| 加载时机 | 浏览器空闲时 | 当前页面加载时 |
| 优先级 | 低 | 高 |
| 适用场景 | 下一个可能访问的页面 | 当前页面必需的资源 |
| 网络影响 | 不阻塞当前页面 | 可能影响当前页面加载 |
使用建议:
- Prefetch: 用于用户可能即将访问的路由 (如首页的"工作台"路由)
- Preload: 用于当前路由的关键依赖 (如首页的图表库)
- 避免滥用: 过多的预加载会浪费带宽,影响实际页面加载
路由守卫中的预加载
// src/router/guard.ts
import type { Router } from 'vue-router'
export const setupRouteGuards = (router: Router) => {
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 预加载可能的下一个路由
if (to.name === 'Dashboard') {
// 预加载工作台可能访问的页面
import('@/views/system/user/index.vue')
import('@/views/system/role/index.vue')
}
next()
})
}组件性能优化
组件性能优化是提升应用运行时性能的关键,RuoYi-Plus-UniApp 通过组件缓存、异步组件、计算属性优化等多种手段,实现了流畅的用户体验。
KeepAlive 组件缓存
<KeepAlive> 是 Vue 3 内置的组件缓存机制,用于缓存动态组件或路由视图,避免重复渲染和数据请求。
路由视图缓存
<!-- src/layouts/components/AppMain/AppMain.vue -->
<template>
<section class="app-main">
<el-scrollbar class="p-4">
<!-- 路由视图 -->
<router-view v-slot="{ Component, route }">
<transition :enter-active-class="animate" mode="out-in">
<!-- KeepAlive 缓存组件 -->
<keep-alive :include="layout.cachedViews.value">
<component :is="Component" v-if="!route.meta.link" :key="route.path" />
</keep-alive>
</transition>
</router-view>
<!-- iframe 处理 -->
<IframeToggle />
</el-scrollbar>
</section>
</template>
<script setup lang="ts">
const route = useRoute()
const layout = useLayout()
const animation = useAnimation()
const animate = ref<string>('')
const animationEnable = ref(layout.animationEnable.value)
// 监听动画启用状态变化
watch(
() => layout.animationEnable.value,
(val: boolean) => {
animationEnable.value = val
animate.value = val ? animation.getRandomAnimation() : animation.defaultAnimate
},
{ immediate: true }
)
// 处理外部链接路由
watchEffect(() => {
if (route.meta.link) {
layout.addIframeView(route)
}
})
</script>KeepAlive 配置说明:
- include: 指定需要缓存的组件名称数组 (通过
layout.cachedViews.value动态管理) - exclude: 指定不需要缓存的组件名称数组 (可选)
- max: 最多缓存的组件实例数量 (可选,默认无限制)
缓存视图管理 (Pinia Store):
// src/stores/modules/layout.ts
import { defineStore } from 'pinia'
export const useLayoutStore = defineStore('layout', () => {
// 缓存的视图列表
const cachedViews = ref<string[]>([])
// 添加缓存视图
const addCachedView = (view: RouteLocationNormalized) => {
if (view.meta.noCache) return
if (view.name && !cachedViews.value.includes(view.name as string)) {
cachedViews.value.push(view.name as string)
}
}
// 删除缓存视图
const delCachedView = (view: RouteLocationNormalized) => {
const index = cachedViews.value.indexOf(view.name as string)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}
// 清空所有缓存视图
const clearCachedViews = () => {
cachedViews.value = []
}
return {
cachedViews,
addCachedView,
delCachedView,
clearCachedViews
}
})路由守卫中管理缓存:
// src/router/guard.ts
router.beforeEach((to, from, next) => {
const layout = useLayout()
// 添加到缓存列表
if (to.meta.noCache !== true && to.name) {
layout.addCachedView(to)
}
next()
})KeepAlive 性能提升:
- 避免重复渲染: 切换路由时,缓存的组件不会被销毁和重新创建
- 保留组件状态: 表单输入、滚动位置、内部状态等都会被保留
- 减少 API 请求: 组件激活时 (activated) 可以选择不重新请求数据
- 内存占用控制: 通过
max属性限制缓存数量,避免内存泄漏
使用建议:
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'
// 组件被缓存激活时
onActivated(() => {
console.log('组件被激活')
// 可选: 刷新数据
// refreshData()
})
// 组件被缓存停用时
onDeactivated(() => {
console.log('组件被停用')
// 可选: 清理定时器、取消请求等
})
</script>异步组件
异步组件是组件级别的懒加载,用于延迟加载大型组件或低优先级组件。
defineAsyncComponent 使用
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// 简单的异步组件
const AsyncChart = defineAsyncComponent(() =>
import('@/components/Chart/LineChart.vue')
)
// 带选项的异步组件
const AsyncTable = defineAsyncComponent({
loader: () => import('@/components/Table/DataTable.vue'),
// 加载时显示的组件
loadingComponent: LoadingSpinner,
// 加载失败时显示的组件
errorComponent: ErrorDisplay,
// 延迟显示加载组件的时间 (ms)
delay: 200,
// 超时时间 (ms)
timeout: 3000
})
</script>
<template>
<div>
<!-- 使用异步组件 -->
<Suspense>
<template #default>
<AsyncChart :data="chartData" />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
<AsyncTable :data="tableData" />
</div>
</template>异步组件适用场景:
- 大型图表组件: ECharts、D3.js 等图表库体积较大,适合异步加载
- 富文本编辑器: Quill、WangEditor 等编辑器,按需加载
- 低优先级组件: 页面底部的组件,可以延迟加载
- 条件渲染组件: 只在特定条件下显示的组件
Suspense 组件
<Suspense> 是 Vue 3 的实验性特性,用于优雅地处理异步组件的加载状态。
<template>
<Suspense>
<!-- 主内容 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中状态 -->
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>注意事项:
<Suspense>目前仍是实验性特性,API 可能会变化- 适合处理多个异步组件的加载状态
- 配合
defineAsyncComponent和async setup()使用
计算属性与侦听器优化
合理使用 computed、watch 和 watchEffect,可以避免不必要的计算和副作用执行。
计算属性缓存
<script setup lang="ts">
import { ref, computed } from 'vue'
const users = ref<User[]>([])
const searchKeyword = ref('')
// ✅ 推荐: 使用计算属性,自动缓存结果
const filteredUsers = computed(() => {
return users.value.filter(user =>
user.name.includes(searchKeyword.value)
)
})
// ❌ 不推荐: 使用方法,每次调用都重新计算
const getFilteredUsers = () => {
return users.value.filter(user =>
user.name.includes(searchKeyword.value)
)
}
</script>
<template>
<div>
<!-- 计算属性: 只在依赖变化时重新计算 -->
<div v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
</div>
<!-- 方法: 每次渲染都重新计算 -->
<div v-for="user in getFilteredUsers()" :key="user.id">
{{ user.name }}
</div>
</div>
</template>计算属性 vs 方法:
| 特性 | 计算属性 | 方法 |
|---|---|---|
| 缓存 | 自动缓存,依赖不变时返回缓存值 | 每次调用都重新执行 |
| 依赖追踪 | 自动追踪响应式依赖 | 无依赖追踪 |
| 适用场景 | 依赖响应式数据的同步计算 | 不需要缓存的计算或异步操作 |
| 性能 | 高 (有缓存) | 低 (无缓存) |
侦听器优化
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const user = ref({ name: '', age: 0 })
const count = ref(0)
// ✅ 推荐: 指定侦听的属性,避免不必要的触发
watch(() => user.value.name, (newName, oldName) => {
console.log(`Name changed: ${oldName} -> ${newName}`)
})
// ❌ 不推荐: 侦听整个对象,任何属性变化都会触发
watch(user, (newUser, oldUser) => {
console.log('User changed')
})
// ✅ 推荐: 使用 immediate: false,避免首次执行
watch(count, (newCount) => {
console.log(`Count: ${newCount}`)
}, { immediate: false })
// ✅ 推荐: 使用 watchEffect,自动收集依赖
watchEffect(() => {
console.log(`Name: ${user.value.name}, Count: ${count.value}`)
})
// ✅ 推荐: 停止侦听器,避免内存泄漏
const stopWatch = watch(count, () => { /* ... */ })
onBeforeUnmount(() => {
stopWatch()
})
</script>侦听器选项:
watch(
source,
callback,
{
immediate: false, // 是否立即执行一次
deep: false, // 是否深度侦听
flush: 'pre', // 回调执行时机: 'pre' | 'post' | 'sync'
onTrack(e) { /* ... */ }, // 依赖追踪时调用
onTrigger(e) { /* ... */ } // 依赖触发时调用
}
)防抖与节流
对于高频触发的事件 (如 resize、scroll、input),使用防抖 (debounce) 和节流 (throttle) 可以显著提升性能。
防抖函数实现
// src/utils/function.ts
/**
* 函数防抖
* 在指定时间内多次调用,只执行最后一次
*/
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number = 300,
immediate: boolean = false
): ((...args: Parameters<T>) => void) => {
let timeout: ReturnType<typeof setTimeout> | null = null
const debounced = function (this: any, ...args: Parameters<T>) {
const context = this
const later = function () {
timeout = null
if (!immediate) {
func.apply(context, args)
}
}
const callNow = immediate && !timeout
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
if (callNow) {
func.apply(context, args)
}
}
debounced.cancel = function () {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
return debounced as (...args: Parameters<T>) => ReturnType<T>
}节流函数实现
/**
* 函数节流
* 在指定时间内,函数最多执行一次
*/
export const throttle = <T extends (...args: any[]) => any>(
func: T,
wait: number = 300,
options: { leading?: boolean; trailing?: boolean } = {}
): ((...args: Parameters<T>) => ReturnType<T>) => {
let timeout: ReturnType<typeof setTimeout> | null = null
let previous = 0
let args: Parameters<T> | null = null
let context: any = null
let result: any
const leading = 'leading' in options ? !!options.leading : true
const trailing = 'trailing' in options ? !!options.trailing : true
const later = function () {
previous = leading === false ? 0 : Date.now()
timeout = null
if (args && context) {
result = func.apply(context, args)
context = args = null
}
}
const throttled = function (this: any, ...currentArgs: Parameters<T>) {
const now = Date.now()
context = this
args = currentArgs
if (!previous && leading === false) {
previous = now
}
const remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(later, remaining)
}
return result
}
throttled.cancel = function () {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = 0
args = context = null
}
return throttled as (...args: Parameters<T>) => ReturnType<T>
}在组件中使用
<script setup lang="ts">
import { debounce, throttle } from '@/utils/function'
// 搜索输入防抖
const handleSearch = debounce((keyword: string) => {
console.log('搜索:', keyword)
// 执行搜索请求
}, 500)
// 窗口调整大小节流
const handleResize = throttle(() => {
console.log('窗口大小变化')
// 重新计算布局
}, 200)
// 滚动事件节流
const handleScroll = throttle(() => {
const scrollTop = window.scrollY
console.log('滚动位置:', scrollTop)
}, 100)
onMounted(() => {
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
// 取消防抖/节流
handleResize.cancel()
handleScroll.cancel()
})
</script>
<template>
<div>
<input type="text" @input="handleSearch($event.target.value)" placeholder="搜索..." />
</div>
</template>防抖 vs 节流:
| 特性 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 执行时机 | 停止触发后执行一次 | 固定时间间隔执行 |
| 适用场景 | 搜索输入、表单验证 | 滚动事件、窗口调整 |
| 性能提升 | 显著减少函数执行次数 | 限制函数执行频率 |
| 用户体验 | 等待用户输入完成 | 实时反馈 |
表格高度自适应
RuoYi-Plus-UniApp 实现了表格高度自适应 Composable,通过动态计算和防抖处理,实现表格高度的自动调整。
// src/composables/useTableHeight.ts
/**
* 表格高度自适应钩子
*
* @param heightAdjustment 高度调整值 (正数减少,负数增加)
* @returns 表格高度和相关方法
*/
export const useTableHeight = (heightAdjustment: number = 0) => {
const tableHeight = ref<number>(500)
const queryFormRef = ref<any>(null)
const layout = useLayout()
/**
* 计算表格高度
*/
const calculateTableHeight = async () => {
await nextTick()
// 获取查询表单高度
const formHeight = queryFormRef.value?.$el?.offsetHeight || 0
// 页面布局元素高度
const navbarHeight = 50
const tagsHeight = layout.tagsView.value ? 34 : 0
const pageContainerPadding = 16
const cardHeaderHeight = 62
const tablePadding = 40
const paginationHeight = 56
const otherPadding = 18
// 计算可用高度
const availableHeight =
window.innerHeight -
navbarHeight -
tagsHeight -
pageContainerPadding -
formHeight -
cardHeaderHeight -
tablePadding -
paginationHeight -
otherPadding -
heightAdjustment
// 设置最小高度
tableHeight.value = Math.max(availableHeight, 200)
}
// 防抖处理的高度计算
let heightCalculationTimer: number | null = null
const debouncedCalculateHeight = (delay: number = 100) => {
if (heightCalculationTimer !== null) {
clearTimeout(heightCalculationTimer)
}
heightCalculationTimer = window.setTimeout(() => {
calculateTableHeight()
heightCalculationTimer = null
}, delay)
}
// 窗口大小变化处理
const handleResize = () => {
debouncedCalculateHeight(150)
}
// 监听窗口大小变化
onMounted(() => {
window.addEventListener('resize', handleResize)
debouncedCalculateHeight(100)
})
onActivated(() => {
window.addEventListener('resize', handleResize)
debouncedCalculateHeight(100)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (heightCalculationTimer !== null) {
clearTimeout(heightCalculationTimer)
}
if (formResizeObserver) {
formResizeObserver.disconnect()
formResizeObserver = null
}
})
// 监听侧边栏状态
watch(
() => layout.sidebar.value.opened,
() => {
debouncedCalculateHeight(240)
}
)
// 监听页签配置
watch(
() => layout.tagsView.value,
() => {
debouncedCalculateHeight(100)
}
)
// 监听搜索表单显示状态
const showSearch = ref(true)
watch(showSearch, () => {
debouncedCalculateHeight(100)
})
// 使用 ResizeObserver 监听表单高度变化
let formResizeObserver: ResizeObserver | null = null
watch(
() => queryFormRef.value?.$el,
(formElement) => {
if (formResizeObserver) {
formResizeObserver.disconnect()
formResizeObserver = null
}
if (formElement) {
formResizeObserver = new ResizeObserver(() => {
debouncedCalculateHeight(100)
})
formResizeObserver.observe(formElement)
}
},
{ immediate: true }
)
return {
tableHeight,
queryFormRef,
calculateTableHeight,
showSearch
}
}使用示例:
<script setup lang="ts">
import { useTableHeight } from '@/composables/useTableHeight'
const { tableHeight, queryFormRef, showSearch } = useTableHeight()
const tableData = ref([])
</script>
<template>
<div class="page-container">
<!-- 查询表单 -->
<el-form ref="queryFormRef" v-show="showSearch" :inline="true">
<el-form-item label="用户名">
<el-input v-model="queryForm.username" placeholder="请输入用户名" />
</el-form-item>
<!-- 更多表单项... -->
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" :height="tableHeight" border>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
</el-table>
<!-- 分页 -->
<el-pagination
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
/>
</div>
</template>性能优化要点:
- 防抖处理: 窗口调整时使用 150ms 防抖,避免频繁计算
- ResizeObserver: 使用现代 API 监听表单高度变化,性能优于轮询
- 生命周期管理: 在组件卸载时清理事件监听和定时器,防止内存泄漏
- 最小高度限制: 确保表格至少有 200px 高度,避免过小导致不可用
图表性能优化
RuoYi-Plus-UniApp 大量使用 ECharts 图表库,通过实例复用、按需加载和优化配置,实现了高性能的图表渲染。
ECharts 按需加载
// src/components/AChart/composables/useChart.ts
import * as echarts from 'echarts/core'
// 按需导入图表类型
import {
BarChart,
LineChart,
PieChart,
ScatterChart,
RadarChart,
MapChart,
TreeChart,
GraphChart,
GaugeChart,
FunnelChart,
ParallelChart,
SankeyChart,
BoxplotChart,
CandlestickChart,
EffectScatterChart,
LinesChart,
HeatmapChart,
PictorialBarChart,
ThemeRiverChart,
SunburstChart,
CustomChart
} from 'echarts/charts'
// 按需导入组件
import {
TitleComponent,
TooltipComponent,
GridComponent,
PolarComponent,
AriaComponent,
ParallelComponent,
LegendComponent,
RadarComponent,
ToolboxComponent,
DataZoomComponent,
VisualMapComponent,
TimelineComponent,
CalendarComponent,
GraphicComponent
} from 'echarts/components'
// 导入渲染器
import { CanvasRenderer } from 'echarts/renderers'
// 注册必需的组件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
BarChart,
LineChart,
PieChart,
CanvasRenderer
// 根据实际需要添加更多组件
])按需加载优势:
- 全量导入 ECharts: ~900KB (gzip 后 ~300KB)
- 按需导入 ECharts: ~200KB (gzip 后 ~65KB)
- 体积减少: 约 78%
图表实例复用
// src/components/AChart/composables/useChart.ts
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
import * as echarts from 'echarts/core'
export const useChart = (chartRef: Ref<HTMLElement | null>) => {
const chartInstance = ref<echarts.ECharts | null>(null)
/**
* 初始化图表实例
*/
const initChart = () => {
if (!chartRef.value) return
// 如果实例已存在,先销毁
if (chartInstance.value) {
chartInstance.value.dispose()
}
// 创建新实例
chartInstance.value = echarts.init(chartRef.value)
}
/**
* 设置图表选项
*/
const setOption = (option: echarts.EChartsOption, notMerge?: boolean) => {
if (!chartInstance.value) {
initChart()
}
chartInstance.value?.setOption(option, notMerge)
}
/**
* 调整图表大小
*/
const resize = () => {
chartInstance.value?.resize()
}
/**
* 销毁图表实例
*/
const dispose = () => {
if (chartInstance.value) {
chartInstance.value.dispose()
chartInstance.value = null
}
}
// 组件挂载时初始化
onMounted(() => {
initChart()
})
// 组件激活时 (KeepAlive)
onActivated(() => {
if (chartInstance.value) {
resize()
}
})
// 组件卸载前销毁
onBeforeUnmount(() => {
dispose()
})
return {
chartInstance,
initChart,
setOption,
resize,
dispose
}
}图表 Resize 优化
// src/utils/function.ts
/**
* 触发图表 resize (防抖版本)
*/
export const triggerChartResize = (() => {
const trigger = debounce(() => {
window.dispatchEvent(new Event('resize'))
}, 100)
return (): void => {
requestAnimationFrame(() => {
trigger()
})
}
})()使用示例:
<script setup lang="ts">
import { useChart } from '@/components/AChart/composables/useChart'
import { triggerChartResize } from '@/utils/function'
const chartRef = ref<HTMLElement | null>(null)
const { setOption, resize } = useChart(chartRef)
// 设置图表选项
const updateChart = () => {
setOption({
title: { text: '示例图表' },
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ data: [120, 200, 150], type: 'line' }]
})
}
// 组件激活时触发 resize
onActivated(() => {
triggerChartResize()
})
// 监听窗口大小变化
const handleResize = throttle(() => {
resize()
}, 200)
onMounted(() => {
window.addEventListener('resize', handleResize)
updateChart()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>大数据量优化
对于大数据量图表,使用数据采样和虚拟渲染技术。
// 大数据量折线图配置
const option: echarts.EChartsOption = {
xAxis: {
type: 'category',
data: largeDataArray.map(item => item.date)
},
yAxis: {
type: 'value'
},
series: [
{
type: 'line',
data: largeDataArray.map(item => item.value),
// 数据采样
sampling: 'lttb', // Largest-Triangle-Three-Buckets 算法
// 大数据量优化
large: true,
largeThreshold: 2000, // 大于 2000 个数据点启用优化
// 符号配置
symbol: 'none', // 不显示数据点标记,提升性能
// 平滑曲线 (可选)
smooth: false
}
],
// 数据区域缩放
dataZoom: [
{
type: 'inside', // 内置型数据区域缩放
start: 0,
end: 100
},
{
type: 'slider', // 滑动条型数据区域缩放
start: 0,
end: 100
}
]
}大数据量优化策略:
- 数据采样: 使用
sampling: 'lttb'算法,在不影响视觉效果的前提下减少数据点 - large 模式: 启用
large: true,使用专门的大数据量渲染策略 - 虚拟渲染: 只渲染可视区域内的数据点
- 禁用动画: 大数据量时禁用
animation,减少渲染开销 - 数据分页: 使用
dataZoom组件,分段加载和显示数据
最佳实践
开发环境优化
HMR 优化
Vite 的 HMR (模块热替换) 已经非常快,但仍可以通过一些配置进一步优化。
// vite.config.ts
export default defineConfig({
server: {
hmr: {
overlay: true, // 显示错误覆盖层
clientPort: 5173 // 客户端 HMR 端口
}
}
})HMR 最佳实践:
- 保持组件独立: 避免在组件中使用全局副作用,确保 HMR 能正确替换
- 使用 setup 语法:
<script setup>对 HMR 支持更好 - 避免循环依赖: 循环依赖会导致 HMR 失效,需要手动刷新页面
- 合理使用 keep-alive: 开发环境下适当减少缓存,便于测试
开发服务器优化
// vite.config.ts
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
strictPort: false, // 端口被占用时自动递增
open: true, // 自动打开浏览器
cors: true, // 允许跨域
// 预热常用文件
warmup: {
clientFiles: [
'./src/layouts/**/*.vue',
'./src/components/**/*.vue',
'./src/views/common/**/*.vue'
]
}
}
})生产环境优化
代码分割策略
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
// 手动配置 chunk 分割
manualChunks: {
// Vue 生态
vue: ['vue', 'vue-router', 'pinia'],
// Element Plus
'element-plus': ['element-plus', '@element-plus/icons-vue'],
// 工具库
utils: ['axios', '@vueuse/core', 'lodash-es'],
// 图表库
echarts: ['echarts'],
// 富文本编辑器
editor: ['@wangeditor/editor', '@wangeditor/editor-for-vue']
},
// 自定义 chunk 文件名
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
},
// chunk 大小警告阈值
chunkSizeWarningLimit: 1500,
// 启用 CSS 代码分割
cssCodeSplit: true,
// 构建目录
outDir: 'dist',
// 静态资源目录
assetsDir: 'assets',
// 小于此阈值的资源内联为 base64
assetsInlineLimit: 4096
}
})代码分割效果:
dist/assets/js/
├── vue-a8f3d9c2.js # 72KB (Vue 生态)
├── element-plus-b4e7f1d5.js # 156KB (Element Plus)
├── utils-c3d5e8a9.js # 48KB (工具库)
├── echarts-d9f2b6c4.js # 287KB (ECharts)
├── editor-e5a8c7d1.js # 124KB (富文本编辑器)
├── index-f6b9d3e2.js # 89KB (主应用代码)
└── [page]-[hash].js # 各页面代码Tree Shaking 优化
确保第三方库支持 Tree Shaking,优先使用 ESM 版本。
// ✅ 推荐: 导入 ESM 版本
import { debounce } from 'lodash-es'
// ❌ 不推荐: 导入 CommonJS 版本 (无法 Tree Shaking)
import _ from 'lodash'
const debounce = _.debounce
// ✅ 推荐: Element Plus 按需导入
import { ElMessage, ElMessageBox } from 'element-plus'
// ❌ 不推荐: 全量导入
import ElementPlus from 'element-plus'资源预加载
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RuoYi-Plus-UniApp</title>
<!-- 预连接到 API 服务器 -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- 预加载关键资源 -->
<link rel="modulepreload" href="/assets/vendor.js" />
<link rel="preload" href="/assets/main.css" as="style" />
<link rel="preload" href="/assets/font.woff2" as="font" type="font/woff2" crossorigin />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>监控与分析
构建分析
# 安装 rollup-plugin-visualizer
pnpm add -D rollup-plugin-visualizer
# vite.config.ts 中配置
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html'
})
]
})
# 构建时生成分析报告
pnpm build:prod性能监控
// src/utils/performance.ts
/**
* 性能监控工具
*/
export class PerformanceMonitor {
/**
* 测量首屏加载时间
*/
static measureFCP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FCP:', entry.startTime, 'ms')
// 上报到监控服务
}
})
observer.observe({ entryTypes: ['paint'] })
}
/**
* 测量最大内容绘制时间
*/
static measureLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime, 'ms')
// 上报到监控服务
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
/**
* 测量首次输入延迟
*/
static measureFID() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const fid = entry.processingStart - entry.startTime
console.log('FID:', fid, 'ms')
// 上报到监控服务
}
})
observer.observe({ entryTypes: ['first-input'] })
}
/**
* 测量累积布局偏移
*/
static measureCLS() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
console.log('CLS:', clsValue)
// 上报到监控服务
}
}
})
observer.observe({ entryTypes: ['layout-shift'] })
}
}
// 在 main.ts 中启用监控
if (import.meta.env.PROD) {
PerformanceMonitor.measureFCP()
PerformanceMonitor.measureLCP()
PerformanceMonitor.measureFID()
PerformanceMonitor.measureCLS()
}常见问题
1. 首屏加载时间过长
问题原因:
- JavaScript 包体积过大
- 未启用代码分割
- 未启用资源压缩
- 网络请求过多
解决方案:
// 1. 启用路由懒加载
{
path: '/dashboard',
component: () => import('@/views/dashboard/index.vue')
}
// 2. 配置代码分割
// vite.config.ts
{
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus']
}
}
}
}
}
// 3. 启用 Gzip/Brotli 压缩
// env/.env.production
VITE_BUILD_COMPRESS='gzip,brotli'
// 4. 使用 HTTP/2 和资源预加载
// nginx.conf
http2 on;
http2_push_preload on;2. 开发环境 HMR 慢
问题原因:
- 依赖预构建配置不当
- 循环依赖导致全量更新
- 未排除不必要的文件
解决方案:
// vite.config.ts
{
optimizeDeps: {
// 明确列出需要预构建的依赖
include: ['vue', 'vue-router', 'pinia', 'element-plus'],
// 排除某些依赖
exclude: ['@iconify/json']
},
server: {
// 预热常用文件
warmup: {
clientFiles: [
'./src/layouts/**/*.vue',
'./src/components/**/*.vue'
]
}
}
}3. 图表渲染卡顿
问题原因:
- 数据量过大
- 未启用数据采样
- 频繁的 resize 操作
解决方案:
// 1. 启用数据采样和大数据量模式
const option = {
series: [{
type: 'line',
sampling: 'lttb',
large: true,
largeThreshold: 2000
}]
}
// 2. 优化 resize 处理
const handleResize = throttle(() => {
chartInstance.resize()
}, 200)
// 3. 使用 requestAnimationFrame
const triggerResize = () => {
requestAnimationFrame(() => {
chartInstance.resize()
})
}4. 内存泄漏
问题原因:
- 未清理事件监听器
- 未清理定时器
- 未销毁组件实例
解决方案:
<script setup lang="ts">
let timer: number | null = null
// 设置定时器
const startTimer = () => {
timer = window.setInterval(() => {
// ...
}, 1000)
}
// 清理定时器
onBeforeUnmount(() => {
if (timer !== null) {
clearInterval(timer)
timer = null
}
})
// 清理事件监听
const handleResize = () => { /* ... */ }
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
// 清理 ResizeObserver
let observer: ResizeObserver | null = null
onMounted(() => {
observer = new ResizeObserver(() => { /* ... */ })
observer.observe(element)
})
onBeforeUnmount(() => {
if (observer) {
observer.disconnect()
observer = null
}
})
</script>5. 打包后白屏
问题原因:
- 路由模式配置错误
- 静态资源路径错误
- 浏览器兼容性问题
解决方案:
// 1. 检查路由 base 配置
// vite.config.ts
{
base: '/admin/' // 根据实际部署路径配置
}
// 2. 检查路由模式
// src/router/router.ts
const router = createRouter({
history: createWebHistory('/admin/'), // 与 base 保持一致
routes: constantRoutes
})
// 3. 检查浏览器兼容性
// package.json
{
"browserslist": [
"Chrome >= 87",
"Edge >= 88",
"Safari >= 14",
"Firefox >= 78"
]
}
// 4. 检查控制台错误
// 使用浏览器开发者工具查看错误信息6. Element Plus 组件样式丢失
问题原因:
- 未正确配置自动导入
- 未导入全局样式
- CSS 加载顺序问题
解决方案:
// 1. 配置组件自动导入
// vite/plugins/components.ts
Components({
resolvers: [
ElementPlusResolver() // 自动导入样式
]
})
// 2. 导入全局样式
// src/main.ts
import 'element-plus/dist/index.css'
// 或在 vite.config.ts 中配置
{
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "element-plus/theme-chalk/src/index.scss" as *;`
}
}
}
}7. 大列表渲染性能问题
问题原因:
- 渲染了过多的 DOM 节点
- 未使用虚拟滚动
- 数据更新频繁
解决方案:
<script setup lang="ts">
// 1. 使用虚拟滚动 (vue-virtual-scroller)
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
})))
</script>
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
<style scoped>
.item {
height: 50px;
padding: 10px;
}
</style><!-- 2. 使用分页或懒加载 -->
<script setup lang="ts">
const pageSize = 50
const currentPage = ref(1)
// 计算当前页数据
const visibleItems = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return allItems.value.slice(start, end)
})
</script>
<template>
<div>
<div v-for="item in visibleItems" :key="item.id">
{{ item.name }}
</div>
<el-pagination
:total="allItems.length"
:page-size="pageSize"
:current-page="currentPage"
@current-change="currentPage = $event"
/>
</div>
</template>总结
前端性能优化是一个持续的过程,需要在开发、构建、部署各个环节都予以重视。RuoYi-Plus-UniApp 通过 Vite 构建优化、路由懒加载、组件缓存、资源压缩、代码分割等多种手段,实现了优秀的性能表现。
核心优化策略回顾:
- 构建优化: Vite + Rollup,按需编译,极速 HMR
- 代码分割: 路由懒加载,手动 chunk 配置,减少首屏加载
- 资源压缩: Gzip/Brotli 双重压缩,体积减少 70%+
- 组件优化: KeepAlive 缓存,异步组件,计算属性优化
- 图表优化: 按需加载,实例复用,大数据量优化
- 监控分析: 性能监控,构建分析,持续改进
遵循本文档介绍的最佳实践,可以帮助开发者构建高性能、高质量的前端应用。
