Skip to content

性能优化

性能优化是 Vue.js 应用开发中的重要环节。本指南提供了全面的性能优化策略和最佳实践。

性能分析工具

Vue DevTools

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

浏览器性能工具

javascript
// 使用 Performance API 测量性能
function measurePerformance(name, fn) {
  performance.mark(`${name}-start`)
  const result = fn()
  performance.mark(`${name}-end`)
  performance.measure(name, `${name}-start`, `${name}-end`)
  
  const measure = performance.getEntriesByName(name)[0]
  console.log(`${name} took ${measure.duration}ms`)
  
  return result
}

// 使用示例
const result = measurePerformance('data-processing', () => {
  return processLargeDataSet(data)
})

自定义性能监控

javascript
// composables/usePerformanceMonitor.js
import { ref, onMounted, onUnmounted } from 'vue'

export function usePerformanceMonitor(componentName) {
  const renderTime = ref(0)
  const mountTime = ref(0)
  
  let startTime = 0
  
  onMounted(() => {
    const endTime = performance.now()
    mountTime.value = endTime - startTime
    
    console.log(`${componentName} mounted in ${mountTime.value}ms`)
  })
  
  function startRender() {
    startTime = performance.now()
  }
  
  function endRender() {
    renderTime.value = performance.now() - startTime
    console.log(`${componentName} rendered in ${renderTime.value}ms`)
  }
  
  return {
    renderTime,
    mountTime,
    startRender,
    endRender
  }
}

组件优化

组件懒加载

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    // 路由级别的代码分割
    component: () => import(/* webpackChunkName: "about" */ '../views/About.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')
      }
    ]
  }
]

export default createRouter({
  history: createWebHistory(),
  routes
})

异步组件

vue
<template>
  <div>
    <h1>主页面</h1>
    
    <!-- 条件加载重型组件 -->
    <Suspense v-if="showChart">
      <template #default>
        <AsyncChart :data="chartData" />
      </template>
      <template #fallback>
        <div class="loading">加载图表中...</div>
      </template>
    </Suspense>
    
    <!-- 延迟加载组件 -->
    <LazyComponent v-if="shouldLoadComponent" />
  </div>
</template>

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

// 异步组件定义
const AsyncChart = defineAsyncComponent({
  loader: () => import('./components/Chart.vue'),
  loadingComponent: () => import('./components/Loading.vue'),
  errorComponent: () => import('./components/Error.vue'),
  delay: 200,
  timeout: 3000
})

// 懒加载组件
const LazyComponent = defineAsyncComponent(() => 
  import('./components/LazyComponent.vue')
)

const showChart = ref(false)
const shouldLoadComponent = ref(false)
const chartData = ref([])

// 延迟加载逻辑
setTimeout(() => {
  shouldLoadComponent.value = true
}, 2000)
</script>

组件缓存

vue
<template>
  <div>
    <!-- 缓存动态组件 -->
    <KeepAlive :include="cachedComponents" :max="10">
      <component :is="currentComponent" :key="componentKey" />
    </KeepAlive>
    
    <!-- 缓存路由组件 -->
    <router-view v-slot="{ Component, route }">
      <KeepAlive :include="['UserProfile', 'ProductList']">
        <component :is="Component" :key="route.fullPath" />
      </KeepAlive>
    </router-view>
  </div>
</template>

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

const currentComponent = ref('ComponentA')
const componentKey = ref(0)

// 动态控制缓存的组件
const cachedComponents = computed(() => {
  const components = ['ComponentA', 'ComponentB']
  // 根据条件决定是否缓存
  if (someCondition.value) {
    components.push('ComponentC')
  }
  return components
})

// 强制刷新组件
function refreshComponent() {
  componentKey.value++
}
</script>

虚拟滚动

vue
<template>
  <div class="virtual-list" ref="containerRef" @scroll="handleScroll">
    <div class="virtual-list__phantom" :style="{ height: totalHeight + 'px' }"></div>
    <div class="virtual-list__content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="virtual-list__item"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item" :index="item.index">
          {{ item.text }}
        </slot>
      </div>
    </div>
  </div>
</template>

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

const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  itemHeight: {
    type: Number,
    default: 50
  },
  containerHeight: {
    type: Number,
    default: 400
  }
})

const containerRef = ref(null)
const scrollTop = ref(0)

// 计算总高度
const totalHeight = computed(() => props.items.length * props.itemHeight)

// 计算可见区域的起始和结束索引
const startIndex = computed(() => {
  return Math.floor(scrollTop.value / props.itemHeight)
})

const endIndex = computed(() => {
  const visibleCount = Math.ceil(props.containerHeight / props.itemHeight)
  return Math.min(startIndex.value + visibleCount + 1, props.items.length)
})

// 计算可见项目
const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, endIndex.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

// 计算偏移量
const offsetY = computed(() => startIndex.value * props.itemHeight)

// 处理滚动事件
function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}

// 滚动到指定项目
function scrollToItem(index) {
  if (containerRef.value) {
    containerRef.value.scrollTop = index * props.itemHeight
  }
}

defineExpose({
  scrollToItem
})
</script>

<style scoped>
.virtual-list {
  height: v-bind('containerHeight + "px"');
  overflow-y: auto;
  position: relative;
}

.virtual-list__phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.virtual-list__content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.virtual-list__item {
  padding: 10px;
  border-bottom: 1px solid #eee;
}
</style>

响应式优化

合理使用响应式 API

javascript
// ✅ 使用 shallowRef 优化大对象
const largeDataSet = shallowRef({
  users: new Array(10000).fill(null).map((_, i) => ({ id: i, name: `User ${i}` })),
  products: new Array(5000).fill(null).map((_, i) => ({ id: i, name: `Product ${i}` }))
})

// ✅ 使用 shallowReactive 优化大数组
const items = shallowReactive([])

// ✅ 使用 markRaw 标记非响应式对象
const chart = markRaw(new Chart(canvas, config))
const map = markRaw(new GoogleMap(element, options))

// ✅ 使用 readonly 保护数据
const readonlyConfig = readonly({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
})

// ✅ 避免不必要的响应式转换
function processData(rawData) {
  // 如果数据只用于计算,不需要响应式
  const processedData = rawData.map(item => ({
    ...item,
    processed: true
  }))
  
  // 只有最终结果需要响应式
  return ref(processedData.filter(item => item.active))
}

计算属性优化

javascript
// ✅ 使用计算属性缓存昂贵的计算
const expensiveValue = computed(() => {
  console.log('Computing expensive value...')
  return items.value.reduce((sum, item) => {
    return sum + item.price * item.quantity
  }, 0)
})

// ✅ 分解复杂的计算属性
const filteredItems = computed(() => {
  return items.value.filter(item => item.category === selectedCategory.value)
})

const sortedItems = computed(() => {
  return [...filteredItems.value].sort((a, b) => {
    return sortOrder.value === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
  })
})

const paginatedItems = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  const end = start + pageSize.value
  return sortedItems.value.slice(start, end)
})

// ❌ 避免在计算属性中进行副作用操作
const badComputed = computed(() => {
  // 不要在计算属性中修改其他状态
  someOtherState.value = 'modified'  // ❌
  
  // 不要在计算属性中进行 API 调用
  fetchData()  // ❌
  
  return someValue.value * 2
})

侦听器优化

javascript
// ✅ 使用 watchEffect 自动收集依赖
watchEffect(() => {
  if (user.value && user.value.id) {
    fetchUserData(user.value.id)
  }
})

// ✅ 使用 watch 精确控制依赖
watch(
  () => [searchText.value, selectedCategory.value],
  ([newSearchText, newCategory], [oldSearchText, oldCategory]) => {
    if (newSearchText !== oldSearchText || newCategory !== oldCategory) {
      debouncedSearch(newSearchText, newCategory)
    }
  },
  { deep: false } // 避免不必要的深度监听
)

// ✅ 使用防抖优化频繁更新
import { debounce } from 'lodash-es'

const debouncedSearch = debounce((searchText, category) => {
  performSearch(searchText, category)
}, 300)

// ✅ 清理侦听器
const stopWatcher = watch(someRef, (newVal) => {
  // 处理逻辑
})

// 在适当的时候停止侦听
onUnmounted(() => {
  stopWatcher()
})

渲染优化

模板优化

vue
<template>
  <!-- ✅ 使用 v-memo 优化列表渲染 -->
  <div
    v-for="item in expensiveList"
    :key="item.id"
    v-memo="[item.id, item.selected, item.updated]"
    class="list-item"
  >
    <ExpensiveComponent :item="item" />
  </div>
  
  <!-- ✅ 使用 v-once 优化静态内容 -->
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
  
  <!-- ✅ 使用 v-show 而不是 v-if(频繁切换) -->
  <div v-show="isVisible" class="toggle-content">
    频繁切换的内容
  </div>
  
  <!-- ✅ 使用 key 强制重新渲染 -->
  <UserForm :key="user.id" :user="user" />
  
  <!-- ✅ 避免在模板中使用复杂表达式 -->
  <div>{{ formattedDate }}</div>
  
  <!-- ❌ 避免这样做 -->
  <!-- <div>{{ new Date(timestamp).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) }}</div> -->
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps(['timestamp', 'user', 'expensiveList'])

// 将复杂逻辑提取到计算属性
const formattedDate = computed(() => {
  return new Date(props.timestamp).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
})
</script>

事件优化

vue
<template>
  <!-- ✅ 使用事件委托 -->
  <div @click="handleContainerClick">
    <button data-action="edit" :data-id="item.id">编辑</button>
    <button data-action="delete" :data-id="item.id">删除</button>
    <button data-action="view" :data-id="item.id">查看</button>
  </div>
  
  <!-- ✅ 使用修饰符优化事件处理 -->
  <form @submit.prevent="handleSubmit">
    <input @keyup.enter="handleEnter" @input.trim="handleInput">
    <button type="submit">提交</button>
  </form>
  
  <!-- ✅ 防抖处理频繁事件 -->
  <input @input="debouncedInput" placeholder="搜索...">
  
  <!-- ✅ 使用 passive 修饰符优化滚动性能 -->
  <div @scroll.passive="handleScroll" class="scroll-container">
    <!-- 滚动内容 -->
  </div>
</template>

<script setup>
import { debounce } from 'lodash-es'

// 事件委托处理
function handleContainerClick(event) {
  const { target } = event
  const action = target.dataset.action
  const id = target.dataset.id
  
  if (action && id) {
    switch (action) {
      case 'edit':
        editItem(id)
        break
      case 'delete':
        deleteItem(id)
        break
      case 'view':
        viewItem(id)
        break
    }
  }
}

// 防抖输入处理
const debouncedInput = debounce((event) => {
  performSearch(event.target.value)
}, 300)

// 节流滚动处理
const throttledScroll = throttle((event) => {
  updateScrollPosition(event.target.scrollTop)
}, 100)
</script>

网络优化

API 请求优化

javascript
// composables/useApi.js
import { ref, computed } from 'vue'
import axios from 'axios'

// 请求缓存
const cache = new Map()

// 请求去重
const pendingRequests = new Map()

export function useApi() {
  const loading = ref(false)
  const error = ref(null)
  
  // 带缓存的请求
  async function request(url, options = {}) {
    const cacheKey = `${url}${JSON.stringify(options)}`
    
    // 检查缓存
    if (cache.has(cacheKey) && !options.force) {
      return cache.get(cacheKey)
    }
    
    // 检查是否有相同的请求正在进行
    if (pendingRequests.has(cacheKey)) {
      return pendingRequests.get(cacheKey)
    }
    
    loading.value = true
    error.value = null
    
    const requestPromise = axios(url, options)
      .then(response => {
        const data = response.data
        // 缓存结果
        cache.set(cacheKey, data)
        return data
      })
      .catch(err => {
        error.value = err
        throw err
      })
      .finally(() => {
        loading.value = false
        pendingRequests.delete(cacheKey)
      })
    
    pendingRequests.set(cacheKey, requestPromise)
    return requestPromise
  }
  
  // 批量请求
  async function batchRequest(requests) {
    loading.value = true
    error.value = null
    
    try {
      const results = await Promise.allSettled(
        requests.map(req => request(req.url, req.options))
      )
      
      return results.map(result => {
        if (result.status === 'fulfilled') {
          return result.value
        } else {
          console.error('Request failed:', result.reason)
          return null
        }
      })
    } finally {
      loading.value = false
    }
  }
  
  // 清除缓存
  function clearCache(pattern) {
    if (pattern) {
      for (const key of cache.keys()) {
        if (key.includes(pattern)) {
          cache.delete(key)
        }
      }
    } else {
      cache.clear()
    }
  }
  
  return {
    loading: computed(() => loading.value),
    error: computed(() => error.value),
    request,
    batchRequest,
    clearCache
  }
}

数据预加载

javascript
// composables/usePreload.js
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'

export function usePreload() {
  const router = useRouter()
  const preloadedData = ref(new Map())
  
  // 预加载路由组件
  function preloadRoute(routeName) {
    const route = router.resolve({ name: routeName })
    if (route.matched.length > 0) {
      const component = route.matched[0].components?.default
      if (typeof component === 'function') {
        component() // 触发动态导入
      }
    }
  }
  
  // 预加载数据
  async function preloadData(key, fetcher) {
    if (!preloadedData.value.has(key)) {
      try {
        const data = await fetcher()
        preloadedData.value.set(key, data)
      } catch (error) {
        console.error(`Failed to preload data for ${key}:`, error)
      }
    }
  }
  
  // 获取预加载的数据
  function getPreloadedData(key) {
    return preloadedData.value.get(key)
  }
  
  // 在鼠标悬停时预加载
  function onHoverPreload(routeName) {
    let timeoutId
    
    return {
      onMouseEnter() {
        timeoutId = setTimeout(() => {
          preloadRoute(routeName)
        }, 100) // 100ms 延迟,避免误触
      },
      onMouseLeave() {
        if (timeoutId) {
          clearTimeout(timeoutId)
        }
      }
    }
  }
  
  return {
    preloadRoute,
    preloadData,
    getPreloadedData,
    onHoverPreload
  }
}

图片优化

vue
<template>
  <!-- 懒加载图片 -->
  <img
    v-for="image in images"
    :key="image.id"
    v-lazy="image.src"
    :alt="image.alt"
    class="lazy-image"
  >
  
  <!-- 响应式图片 -->
  <picture>
    <source media="(min-width: 768px)" :srcset="image.desktop">
    <source media="(min-width: 480px)" :srcset="image.tablet">
    <img :src="image.mobile" :alt="image.alt">
  </picture>
  
  <!-- WebP 支持 -->
  <picture>
    <source type="image/webp" :srcset="image.webp">
    <source type="image/jpeg" :srcset="image.jpeg">
    <img :src="image.fallback" :alt="image.alt">
  </picture>
</template>

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

// 图片懒加载指令
const vLazy = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = binding.value
          img.classList.remove('lazy')
          observer.unobserve(img)
        }
      })
    })
    
    el.classList.add('lazy')
    observer.observe(el)
  }
}

// 图片预加载
function preloadImages(urls) {
  urls.forEach(url => {
    const img = new Image()
    img.src = url
  })
}

// 图片压缩
function compressImage(file, quality = 0.8) {
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    const img = new Image()
    
    img.onload = () => {
      canvas.width = img.width
      canvas.height = img.height
      ctx.drawImage(img, 0, 0)
      
      canvas.toBlob(resolve, 'image/jpeg', quality)
    }
    
    img.src = URL.createObjectURL(file)
  })
}
</script>

<style scoped>
.lazy-image {
  opacity: 0;
  transition: opacity 0.3s;
}

.lazy-image:not(.lazy) {
  opacity: 1;
}
</style>

内存优化

内存泄漏防护

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

export function useCleanup() {
  const cleanupTasks = ref([])
  
  function addCleanupTask(task) {
    cleanupTasks.value.push(task)
  }
  
  function cleanup() {
    cleanupTasks.value.forEach(task => {
      try {
        task()
      } catch (error) {
        console.error('Cleanup task failed:', error)
      }
    })
    cleanupTasks.value = []
  }
  
  onUnmounted(cleanup)
  
  return {
    addCleanupTask,
    cleanup
  }
}

// 使用示例
export function useWebSocket(url) {
  const { addCleanupTask } = useCleanup()
  const socket = ref(null)
  const connected = ref(false)
  
  function connect() {
    socket.value = new WebSocket(url)
    
    socket.value.onopen = () => {
      connected.value = true
    }
    
    socket.value.onclose = () => {
      connected.value = false
    }
    
    // 注册清理任务
    addCleanupTask(() => {
      if (socket.value) {
        socket.value.close()
        socket.value = null
      }
    })
  }
  
  return {
    socket,
    connected,
    connect
  }
}

// 事件监听器清理
export function useEventListener(target, event, handler, options) {
  const { addCleanupTask } = useCleanup()
  
  onMounted(() => {
    target.addEventListener(event, handler, options)
    
    addCleanupTask(() => {
      target.removeEventListener(event, handler, options)
    })
  })
}

// 定时器清理
export function useInterval(callback, delay) {
  const { addCleanupTask } = useCleanup()
  const intervalId = ref(null)
  
  function start() {
    if (intervalId.value) return
    
    intervalId.value = setInterval(callback, delay)
    
    addCleanupTask(() => {
      if (intervalId.value) {
        clearInterval(intervalId.value)
        intervalId.value = null
      }
    })
  }
  
  function stop() {
    if (intervalId.value) {
      clearInterval(intervalId.value)
      intervalId.value = null
    }
  }
  
  return { start, stop }
}

大数据处理

javascript
// utils/dataProcessor.js

// 分批处理大数据
export function processBatches(data, batchSize = 1000, processor) {
  return new Promise((resolve) => {
    const results = []
    let index = 0
    
    function processBatch() {
      const batch = data.slice(index, index + batchSize)
      if (batch.length === 0) {
        resolve(results)
        return
      }
      
      const batchResult = processor(batch)
      results.push(...batchResult)
      index += batchSize
      
      // 使用 setTimeout 让出主线程
      setTimeout(processBatch, 0)
    }
    
    processBatch()
  })
}

// Web Worker 处理
export function processWithWorker(data, workerScript) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(workerScript)
    
    worker.postMessage(data)
    
    worker.onmessage = (event) => {
      resolve(event.data)
      worker.terminate()
    }
    
    worker.onerror = (error) => {
      reject(error)
      worker.terminate()
    }
  })
}

// 使用示例
export function useDataProcessor() {
  const processing = ref(false)
  const progress = ref(0)
  
  async function processLargeDataSet(data) {
    processing.value = true
    progress.value = 0
    
    try {
      const result = await processBatches(
        data,
        1000,
        (batch) => {
          // 处理批次数据
          const processed = batch.map(item => ({
            ...item,
            processed: true,
            timestamp: Date.now()
          }))
          
          // 更新进度
          progress.value = Math.min(100, (progress.value + (1000 / data.length) * 100))
          
          return processed
        }
      )
      
      return result
    } finally {
      processing.value = false
      progress.value = 100
    }
  }
  
  return {
    processing,
    progress,
    processLargeDataSet
  }
}

构建优化

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: {
          // 将 Vue 相关库打包到一个 chunk
          vue: ['vue', 'vue-router', 'pinia'],
          // 将 UI 库打包到一个 chunk
          ui: ['element-plus', '@element-plus/icons-vue'],
          // 将工具库打包到一个 chunk
          utils: ['lodash-es', 'dayjs', 'axios']
        }
      }
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // 生成 source map
    sourcemap: false,
    
    // 设置 chunk 大小警告限制
    chunkSizeWarningLimit: 1000
  },
  
  // 依赖预构建
  optimizeDeps: {
    include: [
      'vue',
      'vue-router',
      'pinia',
      'axios',
      'lodash-es'
    ],
    exclude: [
      // 排除不需要预构建的依赖
    ]
  },
  
  // 服务器配置
  server: {
    // 预热常用文件
    warmup: {
      clientFiles: [
        './src/components/**/*.vue',
        './src/views/**/*.vue'
      ]
    }
  },
  
  // 别名配置
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views'),
      '@utils': resolve(__dirname, 'src/utils')
    }
  }
})

代码分割策略

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

// 按功能模块分割
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '../layouts/AdminLayout.vue'),
    children: [
      {
        path: 'users',
        component: () => import(/* webpackChunkName: "admin-users" */ '../views/admin/Users.vue')
      },
      {
        path: 'settings',
        component: () => import(/* webpackChunkName: "admin-settings" */ '../views/admin/Settings.vue')
      }
    ]
  },
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue')
  }
]

// 预加载关键路由
const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由预加载
router.beforeEach((to, from, next) => {
  // 预加载下一个可能访问的路由
  if (to.name === 'dashboard') {
    import('../views/Profile.vue') // 预加载用户可能访问的页面
  }
  next()
})

export default router

性能监控

性能指标收集

javascript
// utils/performance.js

// 收集 Core Web Vitals
export function collectWebVitals() {
  // Largest Contentful Paint (LCP)
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    const lastEntry = entries[entries.length - 1]
    console.log('LCP:', lastEntry.startTime)
    
    // 发送到分析服务
    sendMetric('LCP', lastEntry.startTime)
  }).observe({ entryTypes: ['largest-contentful-paint'] })
  
  // First Input Delay (FID)
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    entries.forEach(entry => {
      console.log('FID:', entry.processingStart - entry.startTime)
      sendMetric('FID', entry.processingStart - entry.startTime)
    })
  }).observe({ entryTypes: ['first-input'] })
  
  // Cumulative Layout Shift (CLS)
  let clsValue = 0
  new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries()
    entries.forEach(entry => {
      if (!entry.hadRecentInput) {
        clsValue += entry.value
      }
    })
    console.log('CLS:', clsValue)
    sendMetric('CLS', clsValue)
  }).observe({ entryTypes: ['layout-shift'] })
}

// 发送指标到分析服务
function sendMetric(name, value) {
  // 实现发送逻辑
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', JSON.stringify({ name, value }))
  }
}

// Vue 组件性能监控
export function createPerformancePlugin() {
  return {
    install(app) {
      app.config.globalProperties.$performance = {
        mark: (name) => performance.mark(name),
        measure: (name, startMark, endMark) => {
          performance.measure(name, startMark, endMark)
          const measure = performance.getEntriesByName(name)[0]
          console.log(`${name}: ${measure.duration}ms`)
          return measure.duration
        }
      }
      
      // 监控组件渲染时间
      app.mixin({
        beforeCreate() {
          this.$performance?.mark(`${this.$options.name}-create-start`)
        },
        created() {
          this.$performance?.mark(`${this.$options.name}-create-end`)
          this.$performance?.measure(
            `${this.$options.name}-create`,
            `${this.$options.name}-create-start`,
            `${this.$options.name}-create-end`
          )
        },
        beforeMount() {
          this.$performance?.mark(`${this.$options.name}-mount-start`)
        },
        mounted() {
          this.$performance?.mark(`${this.$options.name}-mount-end`)
          this.$performance?.measure(
            `${this.$options.name}-mount`,
            `${this.$options.name}-mount-start`,
            `${this.$options.name}-mount-end`
          )
        }
      })
    }
  }
}

最佳实践总结

开发阶段

  1. 组件设计

    • 保持组件职责单一
    • 合理使用 Props 和 Events
    • 避免过深的组件嵌套
  2. 状态管理

    • 选择合适的响应式 API
    • 避免不必要的响应式转换
    • 合理使用计算属性和侦听器
  3. 模板优化

    • 使用 v-memo 优化列表渲染
    • 避免在模板中使用复杂表达式
    • 合理使用 v-if 和 v-show

构建阶段

  1. 代码分割

    • 按路由分割代码
    • 按功能模块分割
    • 提取公共依赖
  2. 资源优化

    • 压缩代码和资源
    • 使用现代图片格式
    • 启用 gzip 压缩
  3. 缓存策略

    • 设置合适的缓存头
    • 使用 CDN 加速
    • 实现离线缓存

运行时优化

  1. 渲染优化

    • 使用虚拟滚动处理大列表
    • 实现组件懒加载
    • 优化事件处理
  2. 网络优化

    • 实现请求缓存和去重
    • 使用数据预加载
    • 优化 API 调用
  3. 内存管理

    • 及时清理事件监听器
    • 避免内存泄漏
    • 合理使用 KeepAlive

下一步

性能优化是一个持续的过程,需要在开发的各个阶段都保持关注!

基于 Vue.js 官方文档构建的学习宝典