欢迎光临
我们一直在努力

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 优化效果

优化项变化
首屏 JS 1.42 MB → 0.39 MB
FCP 3.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-table 和 el-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(你写 ref、computed,它自动 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'

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

3.5 注意:主题覆写的适配

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

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

3.6 优化效果

优化项变化
Element Plus JS 全量 → 按需(省 ~200KB gzip)
Element Plus CSS 398KB → ~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-table、el-form、el-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:

指标优化前优化后提升
FCP 3.2s 0.9s -72%
LCP 4.1s 1.2s -71%
TTI 3.8s 1.1s -71%
首屏 JS 1.42 MB 0.21 MB -85%
总 CSS 398 KB 62 KB -84%
构建产物 1.82 MB 0.58 MB -68%
Lighthouse 65 92 +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 截图和开发截图放到一起,量化像素一致度。

赞(0)
未经允许不得转载:171主机测评 » Vue3 还原一个企业级后台-12-性能优化
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址