Vue3 还原一个企业级后台-12-性能优化

性能优化:路由懒加载、组件按需引入

原型也要快。前面三篇业务模块写完,页面一多,首屏直接飙到 3 秒、打包 1.8MB——虽然 Mock 数据让原型看起来像真的,但加载速度暴露了它是原型。本文用 5 个优化把首屏压到 0.9 秒,打包缩到 580KB。


一、先看现状:优化前的基线数据

优化之前先跑一组基线,否则你不知道优化到底有没有效果。

pnpm build

Vite 的构建输出会直接告诉你包体大小:

dist/index.html                   0.45 kB
dist/assets/index-d25b7a3e.css   398.52 kB  # 全是 Element Plus 的样式
dist/assets/index-c8e1ff01.js    1420.31 kB # 所有页面 + 组件 + Element Plus

Chrome DevTools 的 Lighthouse 跑一遍:

指标优化前
FCP(首屏内容绘制)3.2s
LCP(最大内容绘制)4.1s
TTI(可交互时间)3.8s
总 JS 体积1.42 MB
总 CSS 体积398 KB
Lighthouse 评分65

记下这组数据——每做完一个优化就重新跑一次,看数字变化。性能优化最忌讳"凭感觉",一定要量化。


二、路由懒加载:首屏体积砍掉 60%

这是效果最明显的一步。问题很简单:目前的 router/index.js 是这样写的:

// ❌ 同步 import:所有页面组件在首页就被打包进来了
import ApiRegistry from '@/views/api-manage/ApiRegistry.vue'
import ApiDetail from '@/views/api-manage/ApiDetail.vue'
import ModelHub from '@/views/model-hub/ModelHub.vue'
import ModelDetail from '@/views/model-hub/ModelDetail.vue'
import PublishList from '@/views/model-publish/PublishList.vue'
import PublishDetail from '@/views/model-publish/PublishDetail.vue'
import Login from '@/views/Login.vue'

const routes = [
  { path: '/', redirect: '/api-manage/registry' },
  {
    path: '/',
    component: MainLayout,
    children: [
      { path: 'api-manage/registry', component: ApiRegistry },
      { path: 'api-manage/detail/:id', component: ApiDetail },
      { path: 'model-hub', component: ModelHub },
      { path: 'model-hub/detail/:id', component: ModelDetail },
      { path: 'model-publish', component: PublishList },
      { path: 'model-publish/detail/:id', component: PublishDetail },
    ]
  },
  { path: '/login', component: Login }
]

用户在首页 /api-manage/registry,但浏览器已经把模型汇聚、模型发布、登录页的代码全部下载了——这些页面用户根本没访问。

2.1 改成动态 import

// ✅ 动态 import:Vite 自动拆包,访问时才加载
const routes = [
  { path: '/', redirect: '/api-manage/registry' },
  {
    path: '/',
    component: MainLayout,
    children: [
      {
        path: 'api-manage/registry',
        component: () => import('@/views/api-manage/ApiRegistry.vue')
      },
      {
        path: 'api-manage/detail/:id',
        component: () => import('@/views/api-manage/ApiDetail.vue')
      },
      {
        path: 'model-hub',
        component: () => import('@/views/model-hub/ModelHub.vue')
      },
      {
        path: 'model-hub/detail/:id',
        component: () => import('@/views/model-hub/ModelDetail.vue')
      },
      {
        path: 'model-publish',
        component: () => import('@/views/model-publish/PublishList.vue')
      },
      {
        path: 'model-publish/detail/:id',
        component: () => import('@/views/model-publish/PublishDetail.vue')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/Login.vue')
  }
]

这一改,Vite 会在构建时把每个 import() 调用单独拆成一个 chunk。首屏只加载 MainLayout + ApiRegistry,其他页面的代码在用户点击时才按需加载。

2.2 魔法注释:给 chunk 取个好名字

默认的 chunk 名是数字 hash(如 3a2f1c.js),调试时完全看不出谁是谁。加上魔法注释:

component: () => import(/* webpackChunkName: "api-registry" */ '@/views/api-manage/ApiRegistry.vue')
component: () => import(/* webpackChunkName: "model-hub" */ '@/views/model-hub/ModelHub.vue')
component: () => import(/* webpackChunkName: "model-publish" */ '@/views/model-publish/PublishList.vue')

构建输出变成:

dist/assets/api-registry-3a2f1c.js    42 KB
dist/assets/model-hub-b7e8d4.js       38 KB
dist/assets/model-publish-9f1a3c.js   29 KB
dist/assets/api-detail-5d2b8e.js      24 KB
dist/assets/index-c8e1ff01.js         386 KB  ← 只剩首屏需要的代码

首屏 JS 从 1.42 MB 降到约 386 KB,砍掉 73%。

2.3 优化效果

优化项变化
首屏 JS1.42 MB → 0.39 MB
FCP3.2s → 1.8s

三、Element Plus 按需引入:再砍 200KB

目前项目是全局引入 Element Plus:

// main.js — 全局引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

app.use(ElementPlus)

这意味着即使你只用了 el-tableel-button,浏览器也要下载整个 Element Plus 库(~800KB gzip 后 ~200KB)。再加上 CSS 里的全部组件样式(398KB),加在一起就是 600KB 的冗余。

3.1 安装按需引入插件

pnpm add -D unplugin-vue-components unplugin-auto-import

这两个插件分工明确:

  • unplugin-vue-components:自动按需引入 Element Plus 组件(你写 <el-button>,它自动 import { ElButton } from 'element-plus'
  • unplugin-auto-import:自动按需引入 Composition API(你写 refcomputed,它自动 import { ref, computed } from 'vue'

3.2 配置 vite.config.js

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      imports: ['vue', 'vue-router', 'pinia'],  // 自动引入 ref/reactive/computed/useRouter/useStore 等
      dts: 'src/auto-imports.d.ts'              // 生成类型声明文件
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts'                // 生成组件类型声明文件
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

3.3 修改 main.js

// main.js — 去掉全局引入
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store'

import './mock'
import './styles/index.scss'

const app = createApp(App)
app.use(pinia)
app.use(router)
// ❌ 删掉这行:app.use(ElementPlus)
app.mount('#app')

现在你可以在任何 .vue 文件里直接写 <el-table><el-button><el-dialog>,插件会自动处理引入,不需要手动 import。

3.4 图标也要按需引入

Element Plus 的图标库 @element-plus/icons-vue 有 200+ 个图标,全局引入也会增加包体积。正确的做法是按需引入:

// ❌ 全局引入(打包进所有图标)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

// ✅ 按需引入(只用到的图标才会打包)
import { Plus, Search, Edit, Delete, ArrowLeft } from '@element-plus/icons-vue'

// 在组件中使用
<el-button :icon="Plus">新增</el-button>

3.5 注意:主题覆写的适配

之前我们在第 04 篇用 SCSS 变量覆写了 Element Plus 主题。按需引入后,SCSS 变量仍然生效——因为 unplugin-vue-components 只是控制 JS 组件的引入,不干预 CSS。主题覆写是在 styles/element-override.scss 中定义,通过 vite.config.jscss.preprocessorOptions 注入全局,不受按需引入影响。

// vite.config.js — 主题覆写配置保持不变
css: {
  preprocessorOptions: {
    scss: {
      additionalData: `@use "@/styles/element-override.scss" as *;`
    }
  }
}

3.6 优化效果

优化项变化
Element Plus JS全量 → 按需(省 ~200KB gzip)
Element Plus CSS398KB → ~60KB(只含用到的组件样式)
图标全量 200+ → 按需 8 个

四、路由切换优化:加个进度条和缓存

4.1 NProgress:让加载有反馈

路由懒加载后,首次访问模型汇聚页面会有 300~500ms 的加载时间。这段时间用户盯着白屏,体验很差。加个进度条:

pnpm add nprogress
pnpm add -D @types/nprogress
// router/index.js
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

NProgress.configure({ showSpinner: false, speed: 400, minimum: 0.2 })

router.beforeEach((to, from, next) => {
  NProgress.start()
  next()
})

router.afterEach(() => {
  NProgress.done()
})

页面顶部的蓝色进度条一闪而过,用户知道"正在加载",不会误以为卡死了。

4.2 KeepAlive:不让状态白费

模型汇聚的详情页有 3 个 Tab,用户在"接口参数" Tab 里编辑了几行数据,切到"模型文件" Tab 看了一眼,再切回来——编辑的内容没了。

<!-- MainLayout.vue -->
<template>
  <div class="main-layout">
    <TopBar />
    <div class="layout-body">
      <SideNav />
      <div class="content-area">
        <router-view v-slot="{ Component }">
          <keep-alive :include="cachedViews">
            <component :is="Component" />
          </keep-alive>
        </router-view>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 只缓存需要保持状态的页面
const cachedViews = ref([
  'ModelHub',        // 模型汇聚列表(搜索条件保持)
  'ApiRegistry',     // API 注册列表(筛选状态保持)
  'ModelDetail'      // 模型详情(Tab 状态保持)
])
</script>

include 数组按组件 name 匹配。未在数组中的页面不会缓存,切走即销毁,不占内存。


五、构建优化:进一步缩小包体积

5.1 拆 vendor chunk

Element Plus 的 el-tableel-formel-dialog 这些组件在多个页面中复用。默认情况下它们可能被打进每个页面的 chunk 里,导致重复打包。用 manualChunks 把它们单独拆出来:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'element-plus': ['element-plus'],           // Element Plus 单独打包
          'vue-vendor': ['vue', 'vue-router', 'pinia'] // Vue 全家桶单独打包
        }
      }
    }
  }
})

这样 Element Plus 的代码只会被打包一次,所有页面共享一个 element-plus-xxx.js chunk。浏览器第二次访问时直接用缓存,不需要重新下载。

5.2 可视化分析:看看到底谁占了大头

拆完还不放心?用 rollup-plugin-visualizer 生成一张可视化图:

pnpm add -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    // ... 其他插件
    visualizer({
      open: true,        // 构建完自动打开浏览器
      gzipSize: true,    // 显示 gzip 后大小
      brotliSize: true   // 显示 brotli 后大小
    })
  ]
})

构建后自动打开 stats.html,一张树状图直观展示每个模块的体积占比。一眼能看到:Element Plus 的表格组件最胖(~80KB),Element Plus 图表库占了 60KB(其实我们没用图表),mockjs 占了 45KB……

5.3 排除未使用的依赖

可视化分析暴露的第一个问题:mockjs 被打进生产包了。第 11 篇我们已经用 import.meta.env.DEV 做了环境判断,但为了确保 tree-shaking 彻底生效,再加一道保险:

// vite.config.js
export default defineConfig({
  define: {
    __MOCK_ENABLED__: false  // 生产环境关闭 Mock
  }
})

同时确保 mock 目录只在开发环境 import,生产构建时 Vite 会直接把这部分代码从产物中剔除。


六、最终效果:用数据说话

所有优化完成后,再跑一次 pnpm build 和 Lighthouse:

指标优化前优化后提升
FCP3.2s0.9s-72%
LCP4.1s1.2s-71%
TTI3.8s1.1s-71%
首屏 JS1.42 MB0.21 MB-85%
总 CSS398 KB62 KB-84%
构建产物1.82 MB0.58 MB-68%
Lighthouse6592+27 分

6.1 各优化项的贡献

优化项体积缩减FCP 改善
路由懒加载~1 MB-1.4s
Element Plus 按需引入~340 KB-0.5s
图标按需引入~80 KB-0.1s
vendor chunk 拆分无直接缩减,但后续访问 0 下载
mock 生产剔除~45 KB-0.1s
NProgress无体积变化,改善心理等待

路由懒加载贡献了最大的改进——它直接把 6 个页面的代码从首屏中移除。Element Plus 按需引入的效果也很显著,CSS 从 398KB 降到 62KB,节省了 84%。


七、性能优化的铁律:三条原则

做完这轮优化,沉淀三条原则:

1. 先量再优。
优化前跑 Lighthouse / pnpm build 记基线,优化后跑了对比。没有数据,你永远不知道"优化够了没"。

2. 改动最小的优化往往收益最大。
路由懒加载只改了路由定义(10 行代码),首屏体积砍了 73%。Element Plus 按需引入也只加了两个插件配置。真正的性能杀手通常是架构层面的问题,不是某个函数慢了 3ms。

3. 不要过度优化。
Vite 本身的 tree-shaking 和按需加载已经做得很好,别一上来就手写 terser 配置、自定义分包策略。先用工具层的默认优化,不够再补。本项目这 5 个优化做完,Lighthouse 92 分,对于一个原型项目来说已经远超预期。


上一篇:11 - Mock 数据策略:让原型看起来真实

下一篇预告:代码写完、性能达标,最后一件事——怎么证明你还原了 Figma?下一篇用 Playwright + pixelmatch 做自动化像素对比,把 Figma 截图和开发截图放到一起,量化像素一致度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

leoZ231

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值