问题是怎么发现的
官网首页 Lighthouse 评分 42,首屏加载 3.2 秒。用户反馈”白屏时间太长”。打开 Chrome DevTools Network 面板一看:JS bundle 2.4MB,一张 hero 背景图 800KB。
优化后:首屏 0.8 秒,Lighthouse 95。这篇把整个过程和具体配置写清楚。
优化全景图
1 2 3 4 5 6
| 3.2s → 0.8s 的路径:
构建产物瘦身 → 2.4MB → 800KB (路由懒加载 + Tree Shaking) 资源加载优化 → 800KB → 300KB (Gzip + CDN) 渲染加速 → 白屏消除 (SSR / 骨架屏) 运行时优化 → 交互流畅 (v-memo / 虚拟列表)
|
1. 构建产物瘦身
路由懒加载
1 2 3 4 5 6 7
| import Home from '@/pages/Home.vue' import About from '@/pages/About.vue'
const Home = () => import('@/pages/Home.vue') const About = () => import('@/pages/About.vue')
|
效果:首屏 JS 从 2.4MB 降到 400KB。非首屏路由的代码只在访问时才加载。
组件级异步加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script setup lang="ts"> // 重组件(图表、编辑器、视频播放器)按需加载 const HeavyChart = defineAsyncComponent(() => import('@/components/HeavyChart.vue')) const RichEditor = defineAsyncComponent(() => import('@/components/RichEditor.vue')) </script>
<template> <Suspense> <HeavyChart :data="chartData" /> <template #fallback> <div class="h-80 bg-muted animate-pulse rounded-lg" /> </template> </Suspense> </template>
|
第三方库按需引入
1 2 3 4 5 6 7 8 9 10 11 12 13
| import lodash from 'lodash'
import debounce from 'lodash-es/debounce'
import * as echarts from 'echarts/core' import { BarChart } from 'echarts/charts' import { GridComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers'
echarts.use([BarChart, GridComponent, CanvasRenderer])
|
Vite 构建分析
1 2 3 4 5 6 7 8 9 10 11
| npm i -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'
export default { plugins: [ visualizer({ open: true, gzipSize: true }) ] }
|
一张图看清楚谁在占体积,优先砍大户。
2. 资源加载优化
图片:WebP + 响应式 + 懒加载
1 2 3 4 5 6 7 8 9 10 11
| <template> <picture> <source srcset="/images/hero.webp" type="image/webp" /> <img src="/images/hero.jpg" alt="hero" loading="lazy" decoding="async" /> </picture> </template>
|
WebP 比 JPEG 小 30-50%。loading="lazy" 让屏幕外的图片不加载。
Gzip / Brotli 压缩
1 2 3 4 5 6 7 8 9 10
| gzip on; gzip_types text/css application/javascript image/svg+xml; gzip_min_length 1000; gzip_comp_level 6;
brotli on; brotli_types text/css application/javascript; brotli_comp_level 6;
|
2.4MB 的 JS 压缩后大约 600KB。
CDN + 强缓存
1 2 3 4 5 6 7 8
| location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; }
location /images/ { expires 30d; }
|
文件名带 hash(Vite 默认行为),内容变了自动更新,不需要担心缓存过期。
预加载关键资源
1 2 3
| <link rel="preload" href="/fonts/inter-var.woff2" as="font" crossorigin /> <link rel="preconnect" href="https://api.example.com" />
|
首屏需要的字体和 API 域名提前建立连接。
3. 渲染加速
SSR / SSG(Nuxt)
Nuxt 的 SSR 让首屏 HTML 直接包含内容,不依赖 JS 执行。SEO 和首屏体验质的飞跃:
1 2 3 4 5 6 7 8 9 10
| export default defineNuxtConfig({ ssr: true, nitro: { prerender: { routes: ['/', '/about'], crawlLinks: true, } } })
|
骨架屏
不能用 SSR 的场景,至少给个骨架屏,减少用户感知的白屏:
1 2 3 4 5 6 7 8 9 10
| <template> <Suspense> <ProductList /> <template #fallback> <div v-for="i in 4" :key="i" class="animate-pulse"> <div class="h-48 bg-muted rounded-lg" /> </div> </template> </Suspense> </template>
|
4. 运行时优化
v-memo:跳过不需要更新的子树
1 2 3 4 5 6
| <template> <!-- 只有 item.id 和 selected 变化时才重渲染 --> <div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]"> <ExpensiveComponent :item="item" :active="selected === item.id" /> </div> </template>
|
列表场景,v-memo 能砍掉 80% 的无效渲染。
虚拟列表
1000+ 条数据不要全部渲染 DOM:
1
| npm install vue-virtual-scroller
|
1 2 3 4 5 6 7 8 9 10
| <template> <RecycleScroller :items="list" :item-size="80" key-field="id" v-slot="{ item }" > <ListItem :data="item" /> </RecycleScroller> </template>
|
只渲染可视区域 + 上下缓冲区的 DOM 节点,列表再长也不卡。
shallowRef:大对象避免深度响应式
1 2 3 4 5
| const hugeList = shallowRef<Item[]>([])
hugeList.value = [...hugeList.value, newItem]
|
优化效果对照
| 指标 |
优化前 |
优化后 |
提升 |
| 首屏加载 |
3.2s |
0.8s |
75% |
| JS Bundle (Gzip) |
640KB |
180KB |
72% |
| Lighthouse |
42 |
95 |
+53 |
| FCP |
2.8s |
0.6s |
79% |
| LCP |
4.1s |
1.5s |
63% |
一句话
性能优化不是玄学——分析 → 砍体积 → 优化加载 → 加速渲染 → 减少运行时开销,每一步都有明确的工具和手段。