Skip to content

前端性能优化

介绍

前端性能优化是提升用户体验和系统响应速度的关键环节。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,采用模块化的插件配置方式,实现了高度可定制和可维护的构建流程。

核心配置说明

typescript
// 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,以提升开发和生产环境的加载性能。

typescript
// 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'            // 二维码生成
    ]
  }
}

预构建优势:

  1. 模块格式转换: 将 CommonJS/UMD 转换为 ESM,统一模块规范
  2. 依赖合并: 将多个小文件合并为一个文件,减少 HTTP 请求数量
  3. 缓存优化: 预构建结果会被缓存在 node_modules/.vite 目录,加快二次启动速度
  4. 消除循环依赖: 自动处理依赖之间的循环引用问题

性能提升数据:

  • 首次冷启动: 800ms → 350ms (减少 56%)
  • 二次启动 (有缓存): 200ms → 80ms (减少 60%)
  • 开发环境 HMR: 150ms → 30ms (减少 80%)

插件系统

RuoYi-Plus-UniApp 使用模块化的插件配置方式,将不同功能的插件分散到独立文件中,便于管理和维护。

插件配置入口

typescript
// 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
}

插件加载顺序说明:

  1. vue: 必须最先加载,提供 Vue 3 SFC 解析能力
  2. vueDevTools: 增强开发调试体验
  3. createUnoCss: 处理原子化 CSS,需要在组件导入前处理
  4. createAutoImport: 自动导入 API,影响全局类型定义
  5. createComponents: 自动导入组件,影响组件解析
  6. createCompression: 仅在构建时生效,用于资源压缩
  7. 其他插件: 按功能加载,顺序相对灵活

自动导入配置

自动导入功能通过 unplugin-auto-importunplugin-vue-components 实现,减少手动 import 语句,提升开发效率并优化打包体积。

API 自动导入
typescript
// 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'
  })
}

自动导入效果对比:

vue
<!-- 传统写法 -->
<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 类型定义
组件自动导入
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')
  })
}

组件自动导入效果:

vue
<!-- 传统写法 -->
<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 插件在构建时生成压缩文件。

压缩插件配置

typescript
// 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
}

环境变量配置

bash
# env/.env.production
# 启用 Gzip 压缩
VITE_BUILD_COMPRESS='gzip'

# 同时启用 Gzip 和 Brotli
VITE_BUILD_COMPRESS='gzip,brotli'

压缩效果对比:

文件类型原始大小Gzip 压缩Brotli 压缩Gzip 压缩率Brotli 压缩率
index.html4.2KB1.6KB1.4KB62%67%
main.js523KB145KB128KB72%76%
vendor.js890KB287KB251KB68%72%
app.css156KB28KB24KB82%85%
总计1.57MB462KB404KB71%74%

服务器配置 (Nginx):

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 配置

typescript
// 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)
传统 SCSS350KB280KB52KB
Tailwind CSS420KB180KB38KB
UnoCSS120KB85KB18KB

SCSS 配置

typescript
// 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() 功能实现路由懒加载,所有路由组件都采用异步加载方式。

基础路由懒加载

typescript
// 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 文件

打包结果示例:

bash
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) 通常不使用懒加载,因为它是大多数页面的容器组件,需要立即可用。

typescript
// 同步导入布局组件
import Layout from '@/layouts/Layout.vue'
import HomeLayout from '@/layouts/HomeLayout.vue'

// 路由配置
{
  path: '/',
  component: Layout,  // 同步加载
  children: [
    {
      path: 'dashboard',
      component: () => import('@/views/dashboard/index.vue')  // 异步加载
    }
  ]
}

布局组件同步加载的原因:

  1. 共享依赖: 布局组件被多个路由共享,打包到主 bundle 更高效
  2. 避免闪烁: 立即可用,避免路由切换时的布局加载闪烁
  3. 体积较小: 布局组件通常体积较小,对首屏影响不大

路由配置优化

路由元信息优化

合理配置路由的 meta 信息,可以优化组件缓存和加载策略。

typescript
// 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 配置说明:

typescript
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 支持基于权限的动态路由加载,只加载用户有权访问的路由。

typescript
// 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
}

动态路由加载流程:

  1. 用户登录成功后,获取用户权限信息
  2. 根据权限筛选可访问的路由
  3. 使用 router.addRoute() 动态添加路由
  4. 用户退出时,调用 resetRouter() 清除动态路由

性能优势:

  • 减少路由表大小,提升路由匹配速度
  • 减少不必要的路由组件打包,降低总体积
  • 提升安全性,前端也控制路由访问权限

预加载策略

对于用户可能即将访问的路由,可以使用预加载 (Prefetch) 和预连接 (Preconnect) 策略,提前加载资源。

Webpack/Vite 魔法注释

typescript
// 使用 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:

特性PrefetchPreload
加载时机浏览器空闲时当前页面加载时
优先级
适用场景下一个可能访问的页面当前页面必需的资源
网络影响不阻塞当前页面可能影响当前页面加载

使用建议:

  • Prefetch: 用于用户可能即将访问的路由 (如首页的"工作台"路由)
  • Preload: 用于当前路由的关键依赖 (如首页的图表库)
  • 避免滥用: 过多的预加载会浪费带宽,影响实际页面加载

路由守卫中的预加载

typescript
// 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 内置的组件缓存机制,用于缓存动态组件或路由视图,避免重复渲染和数据请求。

路由视图缓存

vue
<!-- 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):

typescript
// 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
  }
})

路由守卫中管理缓存:

typescript
// 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 属性限制缓存数量,避免内存泄漏

使用建议:

vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

// 组件被缓存激活时
onActivated(() => {
  console.log('组件被激活')
  // 可选: 刷新数据
  // refreshData()
})

// 组件被缓存停用时
onDeactivated(() => {
  console.log('组件被停用')
  // 可选: 清理定时器、取消请求等
})
</script>

异步组件

异步组件是组件级别的懒加载,用于延迟加载大型组件或低优先级组件。

defineAsyncComponent 使用

vue
<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>

异步组件适用场景:

  1. 大型图表组件: ECharts、D3.js 等图表库体积较大,适合异步加载
  2. 富文本编辑器: Quill、WangEditor 等编辑器,按需加载
  3. 低优先级组件: 页面底部的组件,可以延迟加载
  4. 条件渲染组件: 只在特定条件下显示的组件

Suspense 组件

<Suspense> 是 Vue 3 的实验性特性,用于优雅地处理异步组件的加载状态。

vue
<template>
  <Suspense>
    <!-- 主内容 -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- 加载中状态 -->
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

注意事项:

  • <Suspense> 目前仍是实验性特性,API 可能会变化
  • 适合处理多个异步组件的加载状态
  • 配合 defineAsyncComponentasync setup() 使用

计算属性与侦听器优化

合理使用 computedwatchwatchEffect,可以避免不必要的计算和副作用执行。

计算属性缓存

vue
<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 方法:

特性计算属性方法
缓存自动缓存,依赖不变时返回缓存值每次调用都重新执行
依赖追踪自动追踪响应式依赖无依赖追踪
适用场景依赖响应式数据的同步计算不需要缓存的计算或异步操作
性能高 (有缓存)低 (无缓存)

侦听器优化

vue
<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>

侦听器选项:

typescript
watch(
  source,
  callback,
  {
    immediate: false,   // 是否立即执行一次
    deep: false,        // 是否深度侦听
    flush: 'pre',       // 回调执行时机: 'pre' | 'post' | 'sync'
    onTrack(e) { /* ... */ },   // 依赖追踪时调用
    onTrigger(e) { /* ... */ }  // 依赖触发时调用
  }
)

防抖与节流

对于高频触发的事件 (如 resize、scroll、input),使用防抖 (debounce) 和节流 (throttle) 可以显著提升性能。

防抖函数实现

typescript
// 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>
}

节流函数实现

typescript
/**
 * 函数节流
 * 在指定时间内,函数最多执行一次
 */
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>
}

在组件中使用

vue
<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,通过动态计算和防抖处理,实现表格高度的自动调整。

typescript
// 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
  }
}

使用示例:

vue
<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>

性能优化要点:

  1. 防抖处理: 窗口调整时使用 150ms 防抖,避免频繁计算
  2. ResizeObserver: 使用现代 API 监听表单高度变化,性能优于轮询
  3. 生命周期管理: 在组件卸载时清理事件监听和定时器,防止内存泄漏
  4. 最小高度限制: 确保表格至少有 200px 高度,避免过小导致不可用

图表性能优化

RuoYi-Plus-UniApp 大量使用 ECharts 图表库,通过实例复用、按需加载和优化配置,实现了高性能的图表渲染。

ECharts 按需加载

typescript
// 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%

图表实例复用

typescript
// 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 优化

typescript
// src/utils/function.ts

/**
 * 触发图表 resize (防抖版本)
 */
export const triggerChartResize = (() => {
  const trigger = debounce(() => {
    window.dispatchEvent(new Event('resize'))
  }, 100)

  return (): void => {
    requestAnimationFrame(() => {
      trigger()
    })
  }
})()

使用示例:

vue
<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>

大数据量优化

对于大数据量图表,使用数据采样和虚拟渲染技术。

typescript
// 大数据量折线图配置
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
    }
  ]
}

大数据量优化策略:

  1. 数据采样: 使用 sampling: 'lttb' 算法,在不影响视觉效果的前提下减少数据点
  2. large 模式: 启用 large: true,使用专门的大数据量渲染策略
  3. 虚拟渲染: 只渲染可视区域内的数据点
  4. 禁用动画: 大数据量时禁用 animation,减少渲染开销
  5. 数据分页: 使用 dataZoom 组件,分段加载和显示数据

最佳实践

开发环境优化

HMR 优化

Vite 的 HMR (模块热替换) 已经非常快,但仍可以通过一些配置进一步优化。

typescript
// vite.config.ts
export default defineConfig({
  server: {
    hmr: {
      overlay: true,      // 显示错误覆盖层
      clientPort: 5173    // 客户端 HMR 端口
    }
  }
})

HMR 最佳实践:

  1. 保持组件独立: 避免在组件中使用全局副作用,确保 HMR 能正确替换
  2. 使用 setup 语法: <script setup> 对 HMR 支持更好
  3. 避免循环依赖: 循环依赖会导致 HMR 失效,需要手动刷新页面
  4. 合理使用 keep-alive: 开发环境下适当减少缓存,便于测试

开发服务器优化

typescript
// 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'
      ]
    }
  }
})

生产环境优化

代码分割策略

typescript
// 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
  }
})

代码分割效果:

bash
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 版本。

typescript
// ✅ 推荐: 导入 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'

资源预加载

html
<!-- 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>

监控与分析

构建分析

bash
# 安装 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

性能监控

typescript
// 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 包体积过大
  • 未启用代码分割
  • 未启用资源压缩
  • 网络请求过多

解决方案:

typescript
// 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 慢

问题原因:

  • 依赖预构建配置不当
  • 循环依赖导致全量更新
  • 未排除不必要的文件

解决方案:

typescript
// 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 操作

解决方案:

typescript
// 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. 内存泄漏

问题原因:

  • 未清理事件监听器
  • 未清理定时器
  • 未销毁组件实例

解决方案:

vue
<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. 打包后白屏

问题原因:

  • 路由模式配置错误
  • 静态资源路径错误
  • 浏览器兼容性问题

解决方案:

typescript
// 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 加载顺序问题

解决方案:

typescript
// 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 节点
  • 未使用虚拟滚动
  • 数据更新频繁

解决方案:

vue
<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>
vue
<!-- 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 构建优化、路由懒加载、组件缓存、资源压缩、代码分割等多种手段,实现了优秀的性能表现。

核心优化策略回顾:

  1. 构建优化: Vite + Rollup,按需编译,极速 HMR
  2. 代码分割: 路由懒加载,手动 chunk 配置,减少首屏加载
  3. 资源压缩: Gzip/Brotli 双重压缩,体积减少 70%+
  4. 组件优化: KeepAlive 缓存,异步组件,计算属性优化
  5. 图表优化: 按需加载,实例复用,大数据量优化
  6. 监控分析: 性能监控,构建分析,持续改进

遵循本文档介绍的最佳实践,可以帮助开发者构建高性能、高质量的前端应用。