性能优化路由懒加载、组件按需引入原型也要快。前面三篇业务模块写完页面一多首屏直接飙到 3 秒、打包 1.8MB——虽然 Mock 数据让原型看起来像真的但加载速度暴露了它是原型。本文用 5 个优化把首屏压到 0.9 秒打包缩到 580KB。一、先看现状优化前的基线数据优化之前先跑一组基线否则你不知道优化到底有没有效果。pnpmbuildVite 的构建输出会直接告诉你包体大小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 PlusChrome DevTools 的 Lighthouse 跑一遍指标优化前FCP首屏内容绘制3.2sLCP最大内容绘制4.1sTTI可交互时间3.8s总 JS 体积1.42 MB总 CSS 体积398 KBLighthouse 评分65记下这组数据——每做完一个优化就重新跑一次看数字变化。性能优化最忌讳凭感觉一定要量化。二、路由懒加载首屏体积砍掉 60%这是效果最明显的一步。问题很简单目前的router/index.js是这样写的// ❌ 同步 import所有页面组件在首页就被打包进来了importApiRegistryfrom/views/api-manage/ApiRegistry.vueimportApiDetailfrom/views/api-manage/ApiDetail.vueimportModelHubfrom/views/model-hub/ModelHub.vueimportModelDetailfrom/views/model-hub/ModelDetail.vueimportPublishListfrom/views/model-publish/PublishList.vueimportPublishDetailfrom/views/model-publish/PublishDetail.vueimportLoginfrom/views/Login.vueconstroutes[{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// ✅ 动态 importVite 自动拆包访问时才加载constroutes[{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。首屏只加载MainLayoutApiRegistry其他页面的代码在用户点击时才按需加载。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 MBFCP3.2s → 1.8s三、Element Plus 按需引入再砍 200KB目前项目是全局引入Element Plus// main.js — 全局引入importElementPlusfromelement-plusimportelement-plus/dist/index.cssapp.use(ElementPlus)这意味着即使你只用了el-table和el-button浏览器也要下载整个 Element Plus 库~800KB gzip 后 ~200KB。再加上 CSS 里的全部组件样式398KB加在一起就是 600KB 的冗余。3.1 安装按需引入插件pnpmadd-Dunplugin-vue-components unplugin-auto-import这两个插件分工明确unplugin-vue-components自动按需引入 Element Plus 组件你写el-button它自动import { ElButton } from element-plusunplugin-auto-import自动按需引入 Composition API你写ref、computed它自动import { ref, computed } from vue3.2 配置 vite.config.js// vite.config.jsimport{defineConfig}fromviteimportvuefromvitejs/plugin-vueimportAutoImportfromunplugin-auto-import/viteimportComponentsfromunplugin-vue-components/viteimport{ElementPlusResolver}fromunplugin-vue-components/resolversimport{fileURLToPath,URL}fromnode:urlexportdefaultdefineConfig({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(newURL(./src,import.meta.url))}}})3.3 修改 main.js// main.js — 去掉全局引入import{createApp}fromvueimportAppfrom./App.vueimportrouterfrom./routerimportpiniafrom./storeimport./mockimport./styles/index.scssconstappcreateApp(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*asElementPlusIconsVuefromelement-plus/icons-vuefor(const[key,component]ofObject.entries(ElementPlusIconsVue)){app.component(key,component)}// ✅ 按需引入只用到的图标才会打包import{Plus,Search,Edit,Delete,ArrowLeft}fromelement-plus/icons-vue// 在组件中使用el-button:iconPlus新增/el-button3.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 gzipElement Plus CSS398KB → ~60KB只含用到的组件样式图标全量 200 → 按需 8 个四、路由切换优化加个进度条和缓存4.1 NProgress让加载有反馈路由懒加载后首次访问模型汇聚页面会有 300~500ms 的加载时间。这段时间用户盯着白屏体验很差。加个进度条pnpmaddnprogresspnpmadd-Dtypes/nprogress// router/index.jsimportNProgressfromnprogressimportnprogress/nprogress.cssNProgress.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 classmain-layout TopBar / div classlayout-body SideNav / div classcontent-area router-view v-slot{ Component } keep-alive :includecachedViews component :isComponent / /keep-alive /router-view /div /div /div /template script setup import { ref } from vue // 只缓存需要保持状态的页面 const cachedViews ref([ ModelHub, // 模型汇聚列表搜索条件保持 ApiRegistry, // API 注册列表筛选状态保持 ModelDetail // 模型详情Tab 状态保持 ]) /scriptinclude数组按组件name匹配。未在数组中的页面不会缓存切走即销毁不占内存。五、构建优化进一步缩小包体积5.1 拆 vendor chunkElement Plus 的el-table、el-form、el-dialog这些组件在多个页面中复用。默认情况下它们可能被打进每个页面的 chunk 里导致重复打包。用manualChunks把它们单独拆出来// vite.config.jsexportdefaultdefineConfig({build:{rollupOptions:{output:{manualChunks:{element-plus:[element-plus],// Element Plus 单独打包vue-vendor:[vue,vue-router,pinia]// Vue 全家桶单独打包}}}}})这样 Element Plus 的代码只会被打包一次所有页面共享一个element-plus-xxx.jschunk。浏览器第二次访问时直接用缓存不需要重新下载。5.2 可视化分析看看到底谁占了大头拆完还不放心用rollup-plugin-visualizer生成一张可视化图pnpmadd-Drollup-plugin-visualizer// vite.config.jsimport{visualizer}fromrollup-plugin-visualizerexportdefaultdefineConfig({plugins:[// ... 其他插件visualizer({open:true,// 构建完自动打开浏览器gzipSize:true,// 显示 gzip 后大小brotliSize:true// 显示 brotli 后大小})]})构建后自动打开stats.html一张树状图直观展示每个模块的体积占比。一眼能看到Element Plus 的表格组件最胖~80KBElement Plus 图表库占了 60KB其实我们没用图表mockjs 占了 45KB……5.3 排除未使用的依赖可视化分析暴露的第一个问题mockjs被打进生产包了。第 11 篇我们已经用import.meta.env.DEV做了环境判断但为了确保 tree-shaking 彻底生效再加一道保险// vite.config.jsexportdefaultdefineConfig({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%Lighthouse659227 分6.1 各优化项的贡献优化项体积缩减FCP 改善路由懒加载~1 MB-1.4sElement Plus 按需引入~340 KB-0.5s图标按需引入~80 KB-0.1svendor chunk 拆分无直接缩减但后续访问 0 下载—mock 生产剔除~45 KB-0.1sNProgress无体积变化改善心理等待—路由懒加载贡献了最大的改进——它直接把 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 截图和开发截图放到一起量化像素一致度。