{T}

性能优化

Vue 3 提供了多种性能优化手段,从响应式系统到编译时优化,再到运行时和打包优化。本章节涵盖完整的性能优化策略。

性能优化体系

code
┌─────────────────────────────────────────────────────────────────────┐
│                       Vue 3 性能优化体系                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌───────────────┐   ┌───────────────┐   ┌───────────────┐         │
│  │   编译时优化   │   │   运行时优化   │   │   打包优化    │         │
│  ├───────────────┤   ├───────────────┤   ├───────────────┤         │
│  │ • 静态提升    │   │ • 响应式优化  │   │ • Tree Shaking │         │
│  │ • Patch Flags │   │ • 组件缓存    │   │ • 代码分割    │         │
│  │ • 预渲染      │   │ • 虚拟列表    │   │ • 懒加载      │         │
│  └───────────────┘   └───────────────┘   └───────────────┘         │
│           │                   │                   │                 │
│           └───────────────────┼───────────────────┘                 │
│                               ▼                                     │
│                    ┌───────────────────┐                           │
│                    │     性能监控       │                           │
│                    │ • DevTools        │                           │
│                    │ • Performance API │                           │
│                    │ • Lighthouse      │                           │
│                    └───────────────────┘                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

响应式优化

使用 shallowRef/shallowReactive

对于大型对象,深层响应式会带来性能开销。

ts
import { shallowRef, shallowReactive, triggerRef } from 'vue'

// ✅ 推荐:大型不可变数据使用 shallowRef
const largeList = shallowRef<BigData[]>([])

// 更新时替换整个数组
largeList.value = newData

// 如果需要强制触发更新
triggerRef(largeList)

// ✅ 推荐:配置对象使用 shallowReactive
const config = shallowReactive({
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000
  },
  features: {
    darkMode: true,
    notifications: false
  }
})

// 只有顶层属性是响应式的
config.api = newApiConfig // ✅ 触发更新
config.api.timeout = 3000 // ❌ 不触发更新

避免不必要的响应式

ts
import { markRaw, ref, reactive } from 'vue'

// ✅ 推荐:静态配置使用 markRaw
const constants = markRaw({
  API_URL: 'https://api.example.com',
  MAX_ITEMS: 100,
  VERSION: '1.0.0'
})

// ✅ 第三方库实例使用 markRaw
const chartInstance = markRaw(new Chart(ctx, config))

// ✅ 纯数据展示不需要响应式
interface TableData {
  id: number
  name: string
  value: number
}

// 如果数据只用于展示,不需要响应式
const tableData = ref<TableData[]>([])
tableData.value = await fetchData() // 获取后直接赋值

// ❌ 不推荐:对静态数据创建响应式
const staticOptions = reactive({
  items: ['a', 'b', 'c'] // 永远不会变化的数据不需要响应式
})

computed vs 方法

Vue SFC
<script setup>
import { ref, computed } from 'vue'

const list = ref([1, 2, 3, 4, 5])

// ✅ 推荐:有缓存,适合模板中多次使用
const filteredList = computed(() => list.value.filter(n => n > 2))

// ✅ 推荐:需要传参时使用方法
function filterByMin(min: number) {
  return list.value.filter(n => n >= min)
}

// 场景对比
const expensiveValue = computed(() => {
  // 复杂计算会被缓存
  return list.value.reduce((sum, n) => sum + n * n, 0)
})
</script>

<template>
  <!-- computed 会被缓存 -->
  <div>Filtered: {{ filteredList }}</div>
  <div>Filtered again: {{ filteredList }}</div>
  
  <!-- 方法每次都会执行 -->
  <div>Min 3: {{ filterByMin(3) }}</div>
</template>

使用 toRef/toRefs 解构

ts
import { reactive, toRefs, toRef } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue'
})

// ✅ 推荐:使用 toRefs 保持响应式
const { count, name } = toRefs(state)

// ✅ 单个属性使用 toRef
const countRef = toRef(state, 'count')

// ❌ 直接解构会失去响应式
const { count: lostCount } = state // 不是响应式

组件优化

使用 v-memo 缓存

Vue SFC
<template>
  <!-- v-memo 接收依赖数组,只有依赖变化时才重新渲染 -->
  <div 
    v-for="item in items" 
    :key="item.id" 
    v-memo="[item.selected, item.count]"
  >
    <!-- 只有 selected 或 count 变化时才更新 -->
    <span>{{ item.name }}</span>
    <span :class="{ active: item.selected }">
      {{ item.count }}
    </span>
  </div>

  <!-- 结合 v-once 处理完全静态的内容 -->
  <div v-for="item in staticItems" :key="item.id" v-memo="[]">
    <!-- 永远不会更新 -->
    {{ item.name }}
  </div>
</template>

使用 v-once

Vue SFC
<template>
  <!-- 只渲染一次,后续不再更新 -->
  <div v-once>
    <h1>{{ staticTitle }}</h1>
    <p>{{ staticDescription }}</p>
  </div>

  <!-- 适用于静态列表、图标等 -->
  <div v-for="icon in icons" :key="icon" v-once>
    <Icon :name="icon" />
  </div>
</template>

合理使用 KeepAlive

Vue SFC
<script setup>
import { ref } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

const currentTab = ref('TabA')
</script>

<template>
  <!-- 缓存组件状态,避免重复渲染 -->
  <KeepAlive>
    <component :is="currentTab" />
  </KeepAlive>
</template>

<!-- 配置包含/排除 -->
<KeepAlive :include="['TabA', 'TabB']" :exclude="['TabC']">
  <component :is="currentTab" />
</KeepAlive>

<!-- 配置最大缓存数 -->
<KeepAlive :max="10">
  <component :is="currentTab" />
</KeepAlive>

组件懒加载

Vue SFC
<script setup>
import { defineAsyncComponent, ref } from 'vue'

// 异步组件定义
const AsyncModal = defineAsyncComponent(() => 
  import('./HeavyModal.vue')
)

// 带配置的异步组件
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <!-- 条件渲染时才加载 -->
  <AsyncModal v-if="showModal" @close="showModal = false" />
</template>

异步组件最佳实践

ts
// components/index.ts - 组件预加载策略
import { defineAsyncComponent } from 'vue'

// 关键路径组件 - 立即加载
import Header from './Header.vue'
import Sidebar from './Sidebar.vue'

// 非关键组件 - 按需加载
const UserPanel = defineAsyncComponent(() => import('./UserPanel.vue'))
const SearchModal = defineAsyncComponent(() => import('./SearchModal.vue'))

// 预加载策略:空闲时预加载可能需要的组件
const preloadComponents = () => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      import('./UserPanel.vue')
      import('./SearchModal.vue')
    })
  }
}

// 路由切换时预加载
router.beforeEach((to, from, next) => {
  const component = to.matched[0]?.components?.default
  if (typeof component === 'function') {
    component() // 预加载组件
  }
  next()
})

编译优化

使用 <script setup>

Vue SFC
<!-- ✅ 推荐:<script setup> 有更好的编译优化 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<!-- 编译后生成更简洁的代码 -->

静态提升

Vue 3 自动将静态内容提升到渲染函数外:

Vue SFC
<template>
  <!-- 静态节点会被提升,只创建一次 -->
  <div class="header">
    <h1>Static Title</h1>
  </div>
  
  <!-- 动态节点正常处理 -->
  <div class="content">
    {{ dynamicContent }}
  </div>
</template>

Patch Flags

Vue 3 使用 Patch Flags 标记动态内容,精确更新:

Vue SFC
<template>
  <!-- 只有 text 需要更新 -->
  <div>{{ message }}</div>
  
  <!-- 只有 class 需要更新 -->
  <div :class="{ active: isActive }">Static</div>
  
  <!-- 只有 props 需要更新 -->
  <div :id="dynamicId">Static</div>
</template>

合理使用模板

Vue SFC
<template>
  <!-- ✅ 推荐:使用 <template> 避免额外 DOM 节点 -->
  <template v-for="item in items" :key="item.id">
    <li>{{ item.name }}</li>
    <li>{{ item.desc }}</li>
  </template>

  <!-- ❌ 不推荐:多余的包装元素 -->
  <div v-for="item in items" :key="item.id">
    <li>{{ item.name }}</li>
    <li>{{ item.desc }}</li>
  </div>
</template>

打包优化

路由懒加载

ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('@/views/Home.vue') // 懒加载
    },
    {
      path: '/about',
      name: 'About',
      component: () => import('@/views/About.vue')
    },
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: () => import(
        /* webpackChunkName: "dashboard" */
        '@/views/Dashboard.vue'
      ),
      children: [
        {
          path: 'analytics',
          component: () => import(
            /* webpackChunkName: "dashboard" */
            '@/views/DashboardAnalytics.vue'
          )
        }
      ]
    }
  ]
})

代码分割策略

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 第三方库分离
          'vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus', '@element-plus/icons-vue'],
          'utils': ['lodash-es', 'dayjs'],
          
          // 业务模块分离
          'dashboard': [
            './src/views/Dashboard.vue',
            './src/views/DashboardAnalytics.vue'
          ]
        }
      }
    },
    // 调整 chunk 大小警告阈值
    chunkSizeWarningLimit: 1000
  }
})

动态导入策略

ts
// 按功能模块分割
const loadModule = async (moduleName: string) => {
  const modules = {
    chart: () => import('./modules/chart'),
    table: () => import('./modules/table'),
    form: () => import('./modules/form')
  }
  
  return modules[moduleName]?.()
}

// 条件加载
const loadPolyfills = async () => {
  if (!('IntersectionObserver' in window)) {
    await import('intersection-observer')
  }
}

Tree Shaking 优化

ts
// ✅ 推荐:具名导入,支持 Tree Shaking
import { ref, computed, watch } from 'vue'
import { debounce, throttle } from 'lodash-es'

// ❌ 不推荐:默认导入整个库
import _ from 'lodash-es'

// ✅ Element Plus 按需导入
import { ElButton, ElInput } from 'element-plus'

// 或使用 unplugin-vue-components 自动导入
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default {
  plugins: [
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
}

渲染优化

虚拟列表

Vue SFC
<script setup>
import { ref } from 'vue'
import { useVirtualList } from '@vueuse/core'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `Item ${i}`
})))

const { list, containerProps, wrapperProps } = useVirtualList(
  items,
  { itemHeight: 50 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 500px; overflow-y: auto;">
    <div v-bind="wrapperProps">
      <div 
        v-for="{ data, index } in list" 
        :key="data.id"
        style="height: 50px;"
      >
        {{ index }}: {{ data.text }}
      </div>
    </div>
  </div>
</template>

使用 requestAnimationFrame

ts
import { ref, onUnmounted } from 'vue'

function useAnimatedCounter(target: number, duration: number = 1000) {
  const count = ref(0)
  let animationFrameId: number | null = null
  
  const animate = (startTime: number) => {
    const elapsed = performance.now() - startTime
    const progress = Math.min(elapsed / duration, 1)
    
    count.value = Math.floor(progress * target)
    
    if (progress < 1) {
      animationFrameId = requestAnimationFrame(() => animate(startTime))
    }
  }
  
  const start = () => {
    const startTime = performance.now()
    animate(startTime)
  }
  
  onUnmounted(() => {
    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId)
    }
  })
  
  return { count, start }
}

防抖与节流

Vue SFC
<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

const searchQuery = ref('')

// 防抖:延迟执行
const debouncedSearch = useDebounceFn((query: string) => {
  // 执行搜索
  console.log('Searching:', query)
}, 300)

// 节流:固定间隔执行
const throttledScroll = useThrottleFn(() => {
  // 处理滚动
  console.log('Scroll position:', window.scrollY)
}, 100)

// 输入处理
const handleInput = (e: Event) => {
  const value = (e.target as HTMLInputElement).value
  searchQuery.value = value
  debouncedSearch(value)
}
</script>

网络优化

数据缓存

ts
import { ref, shallowRef } from 'vue'

// 简单缓存实现
const cache = new Map<string, { data: unknown; timestamp: number }>()

async function useCachedFetch<T>(
  url: string, 
  ttl: number = 5 * 60 * 1000 // 5分钟
): Promise<T> {
  const cached = cache.get(url)
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data as T
  }
  
  const response = await fetch(url)
  const data = await response.json()
  
  cache.set(url, { data, timestamp: Date.now() })
  
  return data
}

// 组件中使用
const userData = shallowRef<User | null>(null)

async function fetchUser(id: number) {
  userData.value = await useCachedFetch<User>(`/api/users/${id}`)
}

请求并行

ts
// ✅ 推荐:并行请求
async function loadDashboardData() {
  const [users, stats, notifications] = await Promise.all([
    fetchUsers(),
    fetchStats(),
    fetchNotifications()
  ])
  
  return { users, stats, notifications }
}

// 带错误处理的并行请求
async function loadDashboardDataSafe() {
  const results = await Promise.allSettled([
    fetchUsers(),
    fetchStats(),
    fetchNotifications()
  ])
  
  return {
    users: results[0].status === 'fulfilled' ? results[0].value : [],
    stats: results[1].status === 'fulfilled' ? results[1].value : null,
    notifications: results[2].status === 'fulfilled' ? results[2].value : []
  }
}

性能监控

Vue DevTools

ts
// 在开发环境启用性能追踪
if (import.meta.env.DEV) {
  // 组件渲染性能
  app.config.performance = true
}

Performance API

ts
// 性能标记
function measureComponentRender(componentName: string) {
  const startMark = `${componentName}-start`
  const endMark = `${componentName}-end`
  
  performance.mark(startMark)
  
  return () => {
    performance.mark(endMark)
    performance.measure(componentName, startMark, endMark)
    
    const measure = performance.getEntriesByName(componentName)[0]
    console.log(`${componentName} render time: ${measure.duration}ms`)
    
    performance.clearMarks(startMark)
    performance.clearMarks(endMark)
    performance.clearMeasures(componentName)
  }
}

// 使用
const endMeasure = measureComponentRender('UserList')
// ... 渲染逻辑
endMeasure()

自定义性能钩子

ts
// composables/usePerformance.ts
import { onMounted, onUpdated } from 'vue'

export function usePerformanceMonitor(componentName: string) {
  let startTime: number
  
  const logRenderTime = (phase: string) => {
    const duration = performance.now() - startTime
    if (duration > 16) { // 超过一帧(60fps)
      console.warn(
        `[Performance] ${componentName} ${phase} took ${duration.toFixed(2)}ms`
      )
    }
  }
  
  onMounted(() => {
    startTime = performance.now()
    logRenderTime('mount')
  })
  
  onUpdated(() => {
    startTime = performance.now()
    logRenderTime('update')
  })
}

Web Vitals 监控

ts
// utils/webVitals.ts
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals'

export function initWebVitals() {
  onCLS(console.log)  // Cumulative Layout Shift
  onFID(console.log)  // First Input Delay
  onLCP(console.log)  // Largest Contentful Paint
  onFCP(console.log)  // First Contentful Paint
  onTTFB(console.log) // Time to First Byte
  
  // 发送到分析服务
  const sendToAnalytics = (metric: Metric) => {
    fetch('/analytics', {
      method: 'POST',
      body: JSON.stringify({
        name: metric.name,
        value: metric.value,
        id: metric.id
      })
    })
  }
}

性能检查清单

开发阶段

  • 使用 shallowRef/shallowReactive 处理大型数据
  • 列表渲染使用唯一 key
  • 避免在模板中使用复杂表达式
  • 使用 computed 缓存计算结果
  • 合理使用 v-memov-once

构建阶段

  • 配置路由懒加载
  • 第三方库按需导入
  • 配置合理的代码分割策略
  • 启用 Gzip/Brotli 压缩
  • 配置资源缓存策略

运行阶段

  • 大列表使用虚拟滚动
  • 图片懒加载
  • 防抖节流高频操作
  • 避免内存泄漏(清理副作用)

下一步

参考资源