第 135 章:性能优化
学习目标
- 掌握 Vue 3 应用性能分析工具
- 学会组件、路由、状态层级的优化
- 理解懒加载、Tree-shaking、缓存策略
- 在项目中实施性能提升方案
一、性能优化概览
二、性能测量
2.1 加载指标
| 指标 | 说明 |
|---|---|
| FCP | First Contentful Paint 首次内容绘制 |
| LCP | Largest Contentful Paint 最大内容绘制 |
| TTI | Time To Interactive 可交互时间 |
| TBT | Total Blocking Time 总阻塞时间 |
| CLS | Cumulative Layout Shift 累积布局偏移 |
2.2 Lighthouse
bash
# Chrome DevTools → Lighthouse
# 或 CLI
pnpm add -D lighthouse
lighthouse http://localhost:3000 --view2.3 Web Vitals
bash
pnpm add web-vitalstypescript
import { onLCP, onFID, onCLS } from 'web-vitals';
onLCP(console.log);
onFID(console.log);
onCLS(console.log);2.4 Vue DevTools
- 组件渲染时间
- 状态变化追踪
- Pinia store 调试
- 路由切换时间
三、组件优化
3.1 v-once
vue
<template>
<!-- 静态内容只渲染一次 -->
<div v-once>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>3.2 v-memo
vue
<template>
<!-- 仅当 list 变化时重渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<Heavy :data="item" />
</div>
</template>3.3 组件缓存
vue
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['HomePage', 'UserPage']" :max="5">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
</template>vue
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
console.log('组件激活');
});
onDeactivated(() => {
console.log('组件停用');
});
</script>3.4 异步组件
typescript
import { defineAsyncComponent } from 'vue';
const Heavy = defineAsyncComponent(() => import('./Heavy.vue'));typescript
// 更细粒度控制
const Heavy = defineAsyncComponent({
loader: () => import('./Heavy.vue'),
loadingComponent: Loading,
errorComponent: Error,
delay: 200,
timeout: 3000
});3.5 函数式组件
typescript
// 简单组件用函数式
import { h, defineComponent } from 'vue';
const Heading = defineComponent((props: { level: number }) => {
return h(`h${props.level}`, null, slots.default);
});四、响应式优化
4.1 合理使用 ref vs reactive
typescript
// ✅ 基本类型用 ref
const count = ref(0);
const name = ref('Tom');
// ✅ 复杂对象用 reactive
const state = reactive({ user: null, list: [] });
// ✅ 不需要响应式用 markRaw
const heavyObj = markRaw({ /* 大对象 */ });4.2 深响应式转化
typescript
import { shallowRef, shallowReactive, triggerRef } from 'vue';
// ref(浅)
const list = shallowRef<Item[]>([]);
// 修改不触发更新,需手动 trigger
list.value = newArray;
triggerRef(list);4.3 避免大型响应式
typescript
// ❌ 整个对象都是响应式
const bigData = reactive({
items: [...1000 items],
meta: { ... }
});
// ✅ 大数组用 shallowRef
const items = shallowRef<Item[]>([]);4.4 toRefs / toRef
typescript
import { toRefs, toRef } from 'vue';
// 解构不丢失响应式
const { user, list } = toRefs(state);
// 单个属性
const name = toRef(state.user, 'name');五、计算属性
5.1 缓存利用
typescript
// ✅ computed 自动缓存
const total = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0);
});
// ❌ methods 每次都计算
function getTotal() {
return items.value.reduce((sum, item) => sum + item.price, 0);
}5.2 拆分 computed
typescript
// ❌ 一个 computed 包含多个逻辑
const stats = computed(() => {
const total = items.value.length;
const active = items.value.filter(i => i.active).length;
const sum = items.value.reduce((s, i) => s + i.price, 0);
return { total, active, sum };
});
// ✅ 拆分
const total = computed(() => items.value.length);
const active = computed(() => items.value.filter(i => i.active).length);
const sum = computed(() => items.value.reduce((s, i) => s + i.price, 0));六、列表渲染
6.1 必须有 key
vue
<!-- ✅ 用稳定 ID -->
<div v-for="user in users" :key="user.id">{{ user.name }}</div>
<!-- ❌ 用 index -->
<div v-for="(user, index) in users" :key="index">{{ user.name }}</div>6.2 虚拟列表
bash
pnpm add vue-virtual-scrollervue
<template>
<RecycleScroller
:items="bigList"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="row">{{ item.name }}</div>
</RecycleScroller>
</template>6.3 分页
vue
<!-- 避免一次渲染大量 -->
<div v-for="item in currentPage" :key="item.id">{{ item.name }}</div>七、路由优化
7.1 懒加载
typescript
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
}
];7.2 动态导入
typescript
const HeavyComponent = defineAsyncComponent(() => import('./Heavy.vue'));7.3 减少路由组件
typescript
// 拆分大组件
const Dashboard = {
Header: () => import('./DashboardHeader.vue'),
Chart: () => import('./DashboardChart.vue'),
List: () => import('./DashboardList.vue')
};八、状态管理
8.1 避免过度全局
typescript
// ❌ 局部状态放全局
const useUiStore = defineStore('ui', () => {
const modalVisible = ref(false); // 不是全局
return { modalVisible };
});
// ✅ 局部用 ref
const modalVisible = ref(false);8.2 大数据分片
typescript
// 分页加载
const list = ref<Item[]>([]);
const page = ref(1);
async function loadMore() {
const res = await api.fetchItems(page.value);
list.value.push(...res.data);
page.value++;
}九、网络优化
9.1 请求合并
typescript
// 多个请求合并
const [users, posts] = await Promise.all([
api.getUsers(),
api.getPosts()
]);9.2 防抖节流
typescript
import { useDebounceFn, useThrottleFn } from '@vueuse/core';
const search = useDebounceFn(async (q: string) => {
const res = await api.search(q);
results.value = res.data;
}, 300);9.3 缓存
typescript
// 按 URL 缓存
const cache = new Map<string, any>();
async function fetchWithCache(url: string) {
if (cache.has(url)) return cache.get(url);
const res = await fetch(url);
const data = await res.json();
cache.set(url, data);
return data;
}9.4 取消请求
typescript
import { watch } from 'vue';
let controller: AbortController;
watch(userId, async (id) => {
controller?.abort();
controller = new AbortController();
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
user.value = await res.json();
});十、图片优化
10.1 懒加载
vue
<template>
<img v-lazy="imageUrl" />
</template>
<script setup>
import { Lazyload } from 'vant';
</script>10.2 响应式图片
vue
<img
:srcset="`
img-320.jpg 320w,
img-640.jpg 640w,
img-1280.jpg 1280w
`"
sizes="(max-width: 640px) 320px, (max-width: 1280px) 640px, 1280px"
src="img-640.jpg"
alt="..."
/>10.3 格式选择
vue
<picture>
<source srcset="img.avif" type="image/avif" />
<source srcset="img.webp" type="image/webp" />
<img src="img.jpg" alt="..." />
</picture>10.4 CDN 与压缩
typescript
// 构建时压缩
import imagemin from 'vite-plugin-imagemin';
export default defineConfig({
plugins: [imagemin({ /* ... */ })]
});十一、构建优化
11.1 代码分割
typescript
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus']
}
}
}
}11.2 库外置
typescript
build: {
rollupOptions: {
external: ['vue'],
output: {
globals: { vue: 'Vue' }
}
}
}11.3 移除 console
typescript
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}11.4 Gzip / Brotli
typescript
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
compression({ algorithm: 'gzip' }),
compression({ algorithm: 'brotliCompress', ext: '.br' })
]
});十二、运行时优化
12.1 防抖
typescript
function debounce<T extends Function>(fn: T, delay: number) {
let timer: number;
return (...args: any[]) => {
clearTimeout(timer);
timer = window.setTimeout(() => fn(...args), delay);
};
}12.2 防抖保存
typescript
const autoSave = useDebounceFn(() => {
api.saveContent(content.value);
}, 1000);
watch(content, () => autoSave());12.3 long task 拆分
typescript
async function processHeavy(items: Item[]) {
const chunks = [];
for (let i = 0; i < items.length; i += 100) {
chunks.push(items.slice(i, i + 100));
}
for (const chunk of chunks) {
await processChunk(chunk);
await new Promise(r => setTimeout(r, 0)); // 让出主线程
}
}12.4 Web Worker
typescript
// worker.ts
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// 主线程
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
result.value = e.data;
};十三、SEO 与 SSR
13.1 Nuxt 3
typescript
// pages/index.vue
<script setup>
useHead({
title: 'My App',
meta: [
{ name: 'description', content: '...' }
]
});
</script>13.2 Vite SSR
typescript
// src/entry-server.ts
import { renderToString } from 'vue/server-renderer';
import { createApp } from './main';
export async function render(url: string) {
const app = createApp();
return await renderToString(app);
}十四、性能监控
typescript
// performance.ts
export function reportWebVitals() {
if (typeof window === 'undefined') return;
const report = (metric: any) => {
console.log(metric);
// 发送到监控服务
navigator.sendBeacon('/api/metrics', JSON.stringify(metric));
};
import('web-vitals').then(({ onLCP, onFID, onCLS }) => {
onLCP(report);
onFID(report);
onCLS(report);
});
}
// 在 main.ts
reportWebVitals();十五、性能检查清单
markdown
## 加载性能
- [ ] 路由懒加载
- [ ] 组件按需导入
- [ ] 图片懒加载
- [ ] 第三方库按需
- [ ] Gzip / Brotli
- [ ] CDN 加速
## 运行时性能
- [ ] v-once / v-memo
- [ ] keep-alive
- [ ] 虚拟列表
- [ ] 防抖节流
- [ ] 大数据分片
- [ ] Web Worker
## 响应式
- [ ] ref vs reactive
- [ ] shallowRef
- [ ] markRaw
- [ ] toRefs
- [ ] 拆分 computed
## 构建
- [ ] 代码分割
- [ ] 移除 console
- [ ] 库外置
- [ ] 压缩图片
- [ ] CSS 提取十六、本章小结
| 优化 | 关键 |
|---|---|
| 组件 | v-once / v-memo / keep-alive |
| 异步 | defineAsyncComponent / 懒加载 |
| 响应式 | shallowRef / markRaw |
| 列表 | 稳定 key / 虚拟列表 |
| 图片 | lazy / responsive / format |
| 网络 | 防抖 / 缓存 / 取消 |
| 构建 | 代码分割 / 压缩 / 库外置 |
动手练习
- 分析现状:用 Lighthouse 评估你的应用
- 路由懒加载:将所有路由改为动态导入
- 虚拟列表:为长列表引入虚拟滚动
- 代码分割:配置 manualChunks 拆分 vendor
- 性能监控:集成 web-vitals 上报数据
推荐阅读
- 📖 Vue 3 性能优化 — 官方
- 📖 Web Vitals — 核心指标
- 📖 Vue Use — 性能工具
- 🌐 Web 性能优化 — MDN
下一章:第 136 章:项目结构与规范 →