Skip to content
第 135 / 250 章前端⏱ 10 分钟阅读

第 135 章:性能优化

学习目标

  • 掌握 Vue 3 应用性能分析工具
  • 学会组件、路由、状态层级的优化
  • 理解懒加载、Tree-shaking、缓存策略
  • 在项目中实施性能提升方案

一、性能优化概览

二、性能测量

2.1 加载指标

指标说明
FCPFirst Contentful Paint 首次内容绘制
LCPLargest Contentful Paint 最大内容绘制
TTITime To Interactive 可交互时间
TBTTotal Blocking Time 总阻塞时间
CLSCumulative Layout Shift 累积布局偏移

2.2 Lighthouse

bash
# Chrome DevTools → Lighthouse
# 或 CLI
pnpm add -D lighthouse
lighthouse http://localhost:3000 --view

2.3 Web Vitals

bash
pnpm add web-vitals
typescript
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-scroller
vue
<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
网络防抖 / 缓存 / 取消
构建代码分割 / 压缩 / 库外置

动手练习

  1. 分析现状:用 Lighthouse 评估你的应用
  2. 路由懒加载:将所有路由改为动态导入
  3. 虚拟列表:为长列表引入虚拟滚动
  4. 代码分割:配置 manualChunks 拆分 vendor
  5. 性能监控:集成 web-vitals 上报数据

推荐阅读


下一章第 136 章:项目结构与规范

本站基于 VitePress 构建 · 由 Codebook 团队维护