Skip to content

Vue 3 性能优化技巧

Vue 3 在性能方面相比 Vue 2 有了显著提升,但合理的优化策略仍然是构建高性能应用的关键。本文将深入探讨 Vue 3 性能优化的各个方面,帮助你构建更快、更流畅的应用。

1. 编译时优化

1.1 静态提升(Static Hoisting)

Vue 3 编译器会自动将静态元素提升到渲染函数外部,避免重复创建。

vue
<template>
  <div>
    <!-- 静态内容会被提升 -->
    <h1>静态标题</h1>
    <p>静态段落</p>
    
    <!-- 动态内容 -->
    <span>{{ message }}</span>
  </div>
</template>

<script>
// 编译后的优化代码(简化版)
const _hoisted_1 = /*#__PURE__*/ createElementVNode("h1", null, "静态标题")
const _hoisted_2 = /*#__PURE__*/ createElementVNode("p", null, "静态段落")

export default {
  setup() {
    return function render() {
      return createElementVNode("div", null, [
        _hoisted_1, // 复用静态节点
        _hoisted_2,
        createElementVNode("span", null, message.value)
      ])
    }
  }
}
</script>

1.2 补丁标记(Patch Flags)

Vue 3 使用补丁标记来标识动态内容,实现精确更新。

vue
<template>
  <div>
    <!-- TEXT: 1 - 文本内容动态 -->
    <span>{{ message }}</span>
    
    <!-- PROPS: 8 - 属性动态 -->
    <div :class="className">内容</div>
    
    <!-- CLASS: 2 - class 动态 -->
    <div :class="{ active: isActive }">按钮</div>
    
    <!-- STYLE: 4 - style 动态 -->
    <div :style="{ color: textColor }">文本</div>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const message = ref('Hello')
    const className = ref('container')
    const isActive = ref(false)
    const textColor = ref('red')
    
    return {
      message,
      className,
      isActive,
      textColor
    }
  }
}
</script>

1.3 树摇优化(Tree Shaking)

javascript
// 按需导入 Vue 功能
import { ref, computed, watch } from 'vue'
// 而不是导入整个 Vue
// import Vue from 'vue'

// 按需导入组件库
import { ElButton, ElInput } from 'element-plus'
// 而不是全量导入
// import ElementPlus from 'element-plus'

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

export default {
  plugins: [
    AutoImport({
      imports: ['vue'],
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
}

2. 响应式优化

2.1 合理使用 ref 和 reactive

javascript
// ✅ 推荐:基本类型使用 ref
const count = ref(0)
const message = ref('hello')
const isLoading = ref(false)

// ✅ 推荐:对象使用 reactive
const user = reactive({
  id: 1,
  name: 'John',
  email: 'john@example.com'
})

// ❌ 避免:对象使用 ref(需要 .value 访问)
const userRef = ref({
  id: 1,
  name: 'John'
})
// 访问时需要 userRef.value.name

// ❌ 避免:基本类型使用 reactive
const state = reactive({
  count: 0 // 不如直接用 ref(0)
})

2.2 使用 shallowRef 和 shallowReactive

javascript
// 对于大型对象或数组,使用浅层响应式
const largeList = shallowRef([
  { id: 1, data: /* 大量数据 */ },
  { id: 2, data: /* 大量数据 */ },
  // ... 更多数据
])

// 更新整个数组来触发响应式
const addItem = (newItem) => {
  largeList.value = [...largeList.value, newItem]
}

// 对于嵌套对象,使用 shallowReactive
const config = shallowReactive({
  theme: 'dark',
  settings: {
    // 这个对象不会是响应式的
    notifications: true,
    autoSave: false
  }
})

// 更新嵌套对象
const updateSettings = (newSettings) => {
  config.settings = { ...config.settings, ...newSettings }
}

2.3 使用 markRaw 避免不必要的响应式

javascript
import { markRaw, reactive } from 'vue'

// 标记不需要响应式的对象
const nonReactiveData = markRaw({
  heavyComputation: () => { /* 复杂计算 */ },
  constants: { PI: 3.14159, E: 2.71828 },
  thirdPartyInstance: new SomeLibrary()
})

const state = reactive({
  user: { name: 'John', age: 30 },
  config: nonReactiveData // 不会被转换为响应式
})

// 对于第三方库实例
const chart = markRaw(new Chart(canvas, options))
const editor = markRaw(new Monaco.Editor())

3. 组件优化

3.1 使用 defineAsyncComponent 异步组件

javascript
// 基本异步组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200, // 延迟显示加载组件
  timeout: 3000, // 超时时间
  suspensible: false,
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry() // 重试
    } else {
      fail() // 失败
    }
  }
})

// 在路由中使用
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  },
  {
    path: '/analytics',
    component: () => import('@/views/Analytics.vue')
  }
]

3.2 使用 KeepAlive 缓存组件

vue
<template>
  <div>
    <!-- 缓存所有组件 -->
    <KeepAlive>
      <component :is="currentComponent" />
    </KeepAlive>
    
    <!-- 选择性缓存 -->
    <KeepAlive :include="['ComponentA', 'ComponentB']">
      <component :is="currentComponent" />
    </KeepAlive>
    
    <!-- 排除特定组件 -->
    <KeepAlive :exclude="['ComponentC']">
      <component :is="currentComponent" />
    </KeepAlive>
    
    <!-- 限制缓存数量 -->
    <KeepAlive :max="10">
      <component :is="currentComponent" />
    </KeepAlive>
  </div>
</template>

<script>
// 在组件中处理缓存生命周期
export default {
  name: 'CacheableComponent',
  activated() {
    console.log('组件被激活')
    // 刷新数据或重新订阅事件
  },
  deactivated() {
    console.log('组件被缓存')
    // 清理定时器或取消订阅
  }
}
</script>

3.3 优化组件 props

javascript
// ✅ 使用具体的 prop 类型
export default {
  props: {
    id: {
      type: Number,
      required: true
    },
    user: {
      type: Object,
      required: true,
      validator: (value) => {
        return value && typeof value.id === 'number'
      }
    },
    status: {
      type: String,
      default: 'pending',
      validator: (value) => {
        return ['pending', 'success', 'error'].includes(value)
      }
    }
  }
}

// ✅ 使用 TypeScript 定义 props
interface Props {
  id: number
  user: User
  status?: 'pending' | 'success' | 'error'
}

const props = defineProps<Props>()

// ✅ 避免传递大型对象,使用计算属性
const userDisplayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

4. 渲染优化

4.1 使用 v-memo 缓存渲染结果

vue
<template>
  <div>
    <!-- 缓存列表项渲染 -->
    <div
      v-for="item in list"
      :key="item.id"
      v-memo="[item.id, item.name, item.status]"
    >
      <h3>{{ item.name }}</h3>
      <p>{{ item.description }}</p>
      <span :class="item.status">{{ item.status }}</span>
    </div>
    
    <!-- 缓存复杂计算结果 -->
    <div v-memo="[expensiveValue]">
      <ExpensiveComponent :data="expensiveValue" />
    </div>
  </div>
</template>

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

export default {
  setup() {
    const list = ref([])
    
    const expensiveValue = computed(() => {
      // 复杂计算
      return list.value.reduce((acc, item) => {
        return acc + item.complexCalculation()
      }, 0)
    })
    
    return { list, expensiveValue }
  }
}
</script>

4.2 虚拟滚动优化长列表

vue
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <div :style="{ height: totalHeight + 'px' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{
          position: 'absolute',
          top: item.top + 'px',
          height: itemHeight + 'px'
        }"
        class="list-item"
      >
        {{ item.data.name }}
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  props: {
    items: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: Number,
      default: 50
    }
  },
  setup(props) {
    const containerHeight = ref(400)
    const scrollTop = ref(0)
    
    const totalHeight = computed(() => {
      return props.items.length * props.itemHeight
    })
    
    const visibleCount = computed(() => {
      return Math.ceil(containerHeight.value / props.itemHeight) + 2
    })
    
    const startIndex = computed(() => {
      return Math.floor(scrollTop.value / props.itemHeight)
    })
    
    const visibleItems = computed(() => {
      const start = startIndex.value
      const end = Math.min(start + visibleCount.value, props.items.length)
      
      return props.items.slice(start, end).map((item, index) => ({
        id: item.id,
        data: item,
        top: (start + index) * props.itemHeight
      }))
    })
    
    const handleScroll = (event) => {
      scrollTop.value = event.target.scrollTop
    }
    
    return {
      totalHeight,
      visibleItems,
      handleScroll
    }
  }
}
</script>

4.3 使用 Suspense 优化异步组件加载

vue
<template>
  <div>
    <Suspense>
      <template #default>
        <AsyncDashboard />
      </template>
      <template #fallback>
        <div class="loading">
          <LoadingSpinner />
          <p>加载中...</p>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'

// 异步组件
const AsyncDashboard = defineAsyncComponent(async () => {
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 1000))
  return import('./Dashboard.vue')
})

export default {
  components: {
    AsyncDashboard,
    LoadingSpinner
  }
}
</script>

5. 状态管理优化

5.1 使用 Pinia 进行状态管理

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    currentUser: null,
    loading: false
  }),
  
  getters: {
    // 缓存计算结果
    activeUsers: (state) => {
      return state.users.filter(user => user.active)
    },
    
    // 参数化 getter
    getUserById: (state) => {
      return (id) => state.users.find(user => user.id === id)
    }
  },
  
  actions: {
    async fetchUsers() {
      if (this.loading) return // 防止重复请求
      
      this.loading = true
      try {
        const users = await api.getUsers()
        this.users = users
      } finally {
        this.loading = false
      }
    },
    
    // 批量更新
    updateUsers(updates) {
      this.$patch((state) => {
        updates.forEach(update => {
          const index = state.users.findIndex(u => u.id === update.id)
          if (index !== -1) {
            Object.assign(state.users[index], update)
          }
        })
      })
    }
  }
})

5.2 优化状态订阅

javascript
// 使用 storeToRefs 保持响应性
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

export default {
  setup() {
    const userStore = useUserStore()
    
    // ✅ 保持响应性
    const { users, currentUser, loading } = storeToRefs(userStore)
    
    // ✅ 方法可以直接解构
    const { fetchUsers, updateUser } = userStore
    
    // ❌ 避免这样做,会失去响应性
    // const { users, currentUser } = userStore
    
    return {
      users,
      currentUser,
      loading,
      fetchUsers,
      updateUser
    }
  }
}

6. 网络请求优化

6.1 请求缓存和去重

javascript
// utils/api.js
class ApiClient {
  constructor() {
    this.cache = new Map()
    this.pendingRequests = new Map()
  }
  
  async get(url, options = {}) {
    const cacheKey = `${url}?${JSON.stringify(options)}`
    
    // 检查缓存
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey)
      if (Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存
        return cached.data
      }
    }
    
    // 检查是否有相同的请求正在进行
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey)
    }
    
    // 发起新请求
    const request = fetch(url, options)
      .then(response => response.json())
      .then(data => {
        // 缓存结果
        this.cache.set(cacheKey, {
          data,
          timestamp: Date.now()
        })
        
        // 清除待处理请求
        this.pendingRequests.delete(cacheKey)
        
        return data
      })
      .catch(error => {
        this.pendingRequests.delete(cacheKey)
        throw error
      })
    
    this.pendingRequests.set(cacheKey, request)
    return request
  }
}

export const api = new ApiClient()

6.2 使用 SWR 模式

javascript
// composables/useSWR.js
import { ref, watchEffect, onUnmounted } from 'vue'

export function useSWR(key, fetcher, options = {}) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  const { 
    refreshInterval = 0,
    revalidateOnFocus = true,
    dedupingInterval = 2000
  } = options
  
  let intervalId = null
  
  const fetchData = async () => {
    if (loading.value) return
    
    loading.value = true
    error.value = null
    
    try {
      const result = await fetcher(key)
      data.value = result
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  // 初始加载
  watchEffect(() => {
    if (key) {
      fetchData()
    }
  })
  
  // 定期刷新
  if (refreshInterval > 0) {
    intervalId = setInterval(fetchData, refreshInterval)
  }
  
  // 窗口聚焦时重新验证
  if (revalidateOnFocus) {
    const handleFocus = () => fetchData()
    window.addEventListener('focus', handleFocus)
    
    onUnmounted(() => {
      window.removeEventListener('focus', handleFocus)
    })
  }
  
  onUnmounted(() => {
    if (intervalId) {
      clearInterval(intervalId)
    }
  })
  
  return {
    data,
    error,
    loading,
    mutate: fetchData
  }
}

// 使用示例
export default {
  setup() {
    const { data: users, error, loading, mutate } = useSWR(
      '/api/users',
      (url) => fetch(url).then(res => res.json()),
      {
        refreshInterval: 30000, // 30秒刷新一次
        revalidateOnFocus: true
      }
    )
    
    return {
      users,
      error,
      loading,
      refreshUsers: mutate
    }
  }
}

7. 构建优化

7.1 代码分割

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    children: [
      {
        path: 'analytics',
        component: () => import('@/views/dashboard/Analytics.vue')
      },
      {
        path: 'reports',
        component: () => import('@/views/dashboard/Reports.vue')
      }
    ]
  }
]

// 手动代码分割
const AdminModule = () => import('@/modules/admin')
const UserModule = () => import('@/modules/user')

7.2 Vite 构建优化

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // 将第三方库分离
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['lodash', 'dayjs']
        }
      }
    },
    
    // 启用 gzip 压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // 生成 source map
    sourcemap: true,
    
    // 设置 chunk 大小警告限制
    chunkSizeWarningLimit: 1000
  },
  
  // 优化依赖预构建
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia'],
    exclude: ['@vueuse/core']
  }
})

8. 性能监控

8.1 性能指标收集

javascript
// utils/performance.js
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.observers = []
  }
  
  // 监控 Core Web Vitals
  observeWebVitals() {
    // First Contentful Paint
    this.observePerformanceEntry('first-contentful-paint', (entry) => {
      this.metrics.FCP = entry.startTime
    })
    
    // Largest Contentful Paint
    this.observeLCP()
    
    // First Input Delay
    this.observeFID()
    
    // Cumulative Layout Shift
    this.observeCLS()
  }
  
  observeLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.metrics.LCP = lastEntry.startTime
    })
    
    observer.observe({ entryTypes: ['largest-contentful-paint'] })
    this.observers.push(observer)
  }
  
  observeFID() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach((entry) => {
        if (entry.name === 'first-input') {
          this.metrics.FID = entry.processingStart - entry.startTime
        }
      })
    })
    
    observer.observe({ entryTypes: ['first-input'] })
    this.observers.push(observer)
  }
  
  observeCLS() {
    let clsValue = 0
    
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach((entry) => {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
          this.metrics.CLS = clsValue
        }
      })
    })
    
    observer.observe({ entryTypes: ['layout-shift'] })
    this.observers.push(observer)
  }
  
  // 监控 Vue 组件性能
  measureComponent(name, fn) {
    const start = performance.now()
    const result = fn()
    const end = performance.now()
    
    this.metrics[`component_${name}`] = end - start
    
    return result
  }
  
  // 发送性能数据
  sendMetrics() {
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/metrics', JSON.stringify(this.metrics))
    } else {
      fetch('/api/metrics', {
        method: 'POST',
        body: JSON.stringify(this.metrics),
        headers: {
          'Content-Type': 'application/json'
        }
      })
    }
  }
  
  disconnect() {
    this.observers.forEach(observer => observer.disconnect())
  }
}

export const performanceMonitor = new PerformanceMonitor()

8.2 Vue DevTools 性能分析

javascript
// 在开发环境中启用性能追踪
if (process.env.NODE_ENV === 'development') {
  app.config.performance = true
}

// 自定义性能标记
export default {
  setup() {
    const heavyComputation = () => {
      performance.mark('heavy-computation-start')
      
      // 执行复杂计算
      const result = complexCalculation()
      
      performance.mark('heavy-computation-end')
      performance.measure(
        'heavy-computation',
        'heavy-computation-start',
        'heavy-computation-end'
      )
      
      return result
    }
    
    return { heavyComputation }
  }
}

9. 最佳实践总结

9.1 开发时优化

javascript
// ✅ 使用 computed 缓存计算结果
const expensiveValue = computed(() => {
  return props.list.filter(item => item.active).length
})

// ✅ 使用 watchEffect 自动追踪依赖
watchEffect(() => {
  if (props.userId) {
    fetchUserData(props.userId)
  }
})

// ✅ 合理使用 nextTick
const updateDOM = async () => {
  state.value = newValue
  await nextTick()
  // DOM 已更新
  measureElement()
}

// ❌ 避免在模板中使用复杂表达式
// <div>{{ items.filter(i => i.active).map(i => i.name).join(', ') }}</div>

// ✅ 使用计算属性
const activeItemNames = computed(() => {
  return items.value
    .filter(i => i.active)
    .map(i => i.name)
    .join(', ')
})

9.2 生产环境优化

javascript
// 环境变量配置
// .env.production
VITE_API_URL=https://api.production.com
VITE_ENABLE_ANALYTICS=true
VITE_LOG_LEVEL=error

// 条件性功能加载
if (import.meta.env.VITE_ENABLE_ANALYTICS === 'true') {
  import('./analytics').then(analytics => {
    analytics.init()
  })
}

// 错误边界
app.config.errorHandler = (err, vm, info) => {
  console.error('Vue error:', err, info)
  
  // 发送错误报告
  if (import.meta.env.PROD) {
    sendErrorReport(err, info)
  }
}

总结

Vue 3 性能优化是一个系统性工程,需要从多个维度进行考虑:

  1. 编译时优化:利用 Vue 3 的编译器优化特性
  2. 响应式优化:合理使用响应式 API,避免不必要的响应式转换
  3. 组件优化:使用异步组件、KeepAlive 等优化组件加载和渲染
  4. 渲染优化:使用 v-memo、虚拟滚动等技术优化大量数据渲染
  5. 状态管理优化:合理设计状态结构,优化状态更新
  6. 网络优化:实现请求缓存、去重等策略
  7. 构建优化:配置代码分割、压缩等构建优化
  8. 性能监控:建立完善的性能监控体系

通过这些优化策略,可以显著提升 Vue 3 应用的性能和用户体验。

相关文章

用代码构建未来,让技术改变世界 🚀 | 专注前端技术,分享开发经验