本章将深入探讨Vue.js应用的性能优化技巧和部署策略,帮助你构建高性能的生产级应用。

9.1 性能分析

性能监控工具

// src/utils/performance.js
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map()
    this.observers = []
    this.init()
  }
  
  init() {
    // 监控页面加载性能
    if ('performance' in window) {
      window.addEventListener('load', () => {
        this.collectLoadMetrics()
      })
    }
    
    // 监控长任务
    if ('PerformanceObserver' in window) {
      this.observeLongTasks()
      this.observeLayoutShifts()
      this.observeLargestContentfulPaint()
    }
  }
  
  collectLoadMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0]
    const paint = performance.getEntriesByType('paint')
    
    const metrics = {
      // 页面加载时间
      loadTime: navigation.loadEventEnd - navigation.loadEventStart,
      // DOM内容加载时间
      domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      // 首次内容绘制
      firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
      // 首次绘制
      firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
      // DNS查询时间
      dnsTime: navigation.domainLookupEnd - navigation.domainLookupStart,
      // TCP连接时间
      tcpTime: navigation.connectEnd - navigation.connectStart,
      // 请求响应时间
      responseTime: navigation.responseEnd - navigation.requestStart,
      // 页面解析时间
      parseTime: navigation.domInteractive - navigation.responseEnd
    }
    
    this.metrics.set('load', metrics)
    this.reportMetrics('load', metrics)
  }
  
  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 50) {
          this.reportMetrics('longTask', {
            duration: entry.duration,
            startTime: entry.startTime,
            name: entry.name
          })
        }
      }
    })
    
    observer.observe({ entryTypes: ['longtask'] })
    this.observers.push(observer)
  }
  
  observeLayoutShifts() {
    let clsValue = 0
    
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
        }
      }
      
      this.metrics.set('cls', clsValue)
    })
    
    observer.observe({ entryTypes: ['layout-shift'] })
    this.observers.push(observer)
  }
  
  observeLargestContentfulPaint() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      
      this.metrics.set('lcp', lastEntry.startTime)
      this.reportMetrics('lcp', { startTime: lastEntry.startTime })
    })
    
    observer.observe({ entryTypes: ['largest-contentful-paint'] })
    this.observers.push(observer)
  }
  
  // 测量组件渲染时间
  measureComponent(name, fn) {
    const start = performance.now()
    const result = fn()
    const end = performance.now()
    
    const duration = end - start
    this.reportMetrics('component', {
      name,
      duration,
      timestamp: start
    })
    
    return result
  }
  
  // 测量异步操作
  async measureAsync(name, asyncFn) {
    const start = performance.now()
    try {
      const result = await asyncFn()
      const end = performance.now()
      
      this.reportMetrics('async', {
        name,
        duration: end - start,
        success: true
      })
      
      return result
    } catch (error) {
      const end = performance.now()
      
      this.reportMetrics('async', {
        name,
        duration: end - start,
        success: false,
        error: error.message
      })
      
      throw error
    }
  }
  
  reportMetrics(type, data) {
    // 发送到分析服务
    if (process.env.NODE_ENV === 'production') {
      this.sendToAnalytics(type, data)
    } else {
      console.log(`[Performance] ${type}:`, data)
    }
  }
  
  sendToAnalytics(type, data) {
    // 发送到Google Analytics、Sentry等
    if (window.gtag) {
      window.gtag('event', 'performance_metric', {
        metric_type: type,
        metric_value: data.duration || data.startTime,
        custom_parameter: JSON.stringify(data)
      })
    }
  }
  
  getMetrics() {
    return Object.fromEntries(this.metrics)
  }
  
  destroy() {
    this.observers.forEach(observer => observer.disconnect())
    this.observers = []
    this.metrics.clear()
  }
}

export const performanceMonitor = new PerformanceMonitor()

Vue DevTools性能分析

// src/utils/vuePerformance.js
import { nextTick } from 'vue'

// 组件性能分析装饰器
export function withPerformanceTracking(component, name) {
  const originalSetup = component.setup
  
  component.setup = function(props, context) {
    const startTime = performance.now()
    
    const result = originalSetup ? originalSetup(props, context) : {}
    
    nextTick(() => {
      const endTime = performance.now()
      console.log(`[Component] ${name} setup time: ${endTime - startTime}ms`)
    })
    
    return result
  }
  
  return component
}

// 渲染性能监控
export function trackRenderPerformance(app) {
  if (process.env.NODE_ENV === 'development') {
    app.config.performance = true
    
    // 监控组件更新
    app.mixin({
      beforeUpdate() {
        this._updateStart = performance.now()
      },
      updated() {
        if (this._updateStart) {
          const duration = performance.now() - this._updateStart
          if (duration > 16) { // 超过一帧的时间
            console.warn(`[Slow Update] ${this.$options.name || 'Anonymous'}: ${duration}ms`)
          }
        }
      }
    })
  }
}

9.2 代码优化

组件优化

<!-- 优化前的组件 -->
<template>
  <div class="user-list">
    <div 
      v-for="user in filteredUsers" 
      :key="user.id"
      class="user-item"
    >
      <img :src="user.avatar" :alt="user.name">
      <div class="user-info">
        <h3>{{ user.name }}</h3>
        <p>{{ formatDate(user.createdAt) }}</p>
        <p>{{ user.posts.length }} 篇文章</p>
        <button @click="toggleFollow(user)">
          {{ user.isFollowing ? '取消关注' : '关注' }}
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { computed, ref } from 'vue'
import { formatDate } from '@/utils/format'

export default {
  name: 'UserList',
  
  props: {
    users: {
      type: Array,
      required: true
    },
    searchQuery: {
      type: String,
      default: ''
    }
  },
  
  setup(props, { emit }) {
    // 问题:每次渲染都会重新计算
    const filteredUsers = computed(() => {
      return props.users.filter(user => 
        user.name.toLowerCase().includes(props.searchQuery.toLowerCase())
      )
    })
    
    // 问题:每次渲染都会创建新函数
    function toggleFollow(user) {
      emit('toggle-follow', user.id)
    }
    
    return {
      filteredUsers,
      formatDate, // 问题:每次渲染都会传递函数
      toggleFollow
    }
  }
}
</script>
<!-- 优化后的组件 -->
<template>
  <div class="user-list">
    <UserItem
      v-for="user in filteredUsers"
      :key="user.id"
      :user="user"
      @toggle-follow="handleToggleFollow"
    />
  </div>
</template>

<script>
import { computed, defineAsyncComponent } from 'vue'
import { useMemoize } from '@/composables/useMemoize'

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

export default {
  name: 'UserList',
  
  components: {
    UserItem
  },
  
  props: {
    users: {
      type: Array,
      required: true
    },
    searchQuery: {
      type: String,
      default: ''
    }
  },
  
  emits: ['toggle-follow'],
  
  setup(props, { emit }) {
    // 使用记忆化优化过滤
    const filteredUsers = useMemoize(
      () => {
        if (!props.searchQuery) return props.users
        
        const query = props.searchQuery.toLowerCase()
        return props.users.filter(user => 
          user.name.toLowerCase().includes(query)
        )
      },
      [() => props.users, () => props.searchQuery]
    )
    
    // 稳定的事件处理函数
    function handleToggleFollow(userId) {
      emit('toggle-follow', userId)
    }
    
    return {
      filteredUsers,
      handleToggleFollow
    }
  }
}
</script>
<!-- UserItem.vue - 拆分的子组件 -->
<template>
  <div class="user-item">
    <LazyImage 
      :src="user.avatar" 
      :alt="user.name"
      class="user-avatar"
    />
    <div class="user-info">
      <h3>{{ user.name }}</h3>
      <p>{{ formattedDate }}</p>
      <p>{{ postsCount }}</p>
      <button 
        @click="$emit('toggle-follow', user.id)"
        :class="followButtonClass"
      >
        {{ followButtonText }}
      </button>
    </div>
  </div>
</template>

<script>
import { computed } from 'vue'
import LazyImage from './LazyImage.vue'
import { formatDate } from '@/utils/format'

export default {
  name: 'UserItem',
  
  components: {
    LazyImage
  },
  
  props: {
    user: {
      type: Object,
      required: true
    }
  },
  
  emits: ['toggle-follow'],
  
  setup(props) {
    // 计算属性缓存
    const formattedDate = computed(() => formatDate(props.user.createdAt))
    const postsCount = computed(() => `${props.user.posts.length} 篇文章`)
    
    const followButtonText = computed(() => 
      props.user.isFollowing ? '取消关注' : '关注'
    )
    
    const followButtonClass = computed(() => ({
      'btn': true,
      'btn-primary': !props.user.isFollowing,
      'btn-secondary': props.user.isFollowing
    }))
    
    return {
      formattedDate,
      postsCount,
      followButtonText,
      followButtonClass
    }
  }
}
</script>

记忆化Composable

// src/composables/useMemoize.js
import { ref, computed, watch } from 'vue'

export function useMemoize(fn, deps) {
  const cache = ref(new Map())
  
  const memoizedValue = computed(() => {
    // 生成缓存键
    const key = JSON.stringify(deps.map(dep => 
      typeof dep === 'function' ? dep() : dep
    ))
    
    // 检查缓存
    if (cache.value.has(key)) {
      return cache.value.get(key)
    }
    
    // 计算新值
    const result = fn()
    
    // 缓存结果
    cache.value.set(key, result)
    
    // 限制缓存大小
    if (cache.value.size > 100) {
      const firstKey = cache.value.keys().next().value
      cache.value.delete(firstKey)
    }
    
    return result
  })
  
  return memoizedValue
}

// 使用示例
export function useExpensiveCalculation(data, filters) {
  return useMemoize(
    () => {
      // 昂贵的计算
      return data.value
        .filter(item => filters.value.category ? item.category === filters.value.category : true)
        .filter(item => filters.value.search ? item.name.includes(filters.value.search) : true)
        .sort((a, b) => {
          switch (filters.value.sortBy) {
            case 'name': return a.name.localeCompare(b.name)
            case 'date': return new Date(b.createdAt) - new Date(a.createdAt)
            case 'price': return b.price - a.price
            default: return 0
          }
        })
    },
    [data, filters]
  )
}

虚拟滚动

<!-- src/components/VirtualList.vue -->
<template>
  <div 
    ref="containerRef"
    class="virtual-list"
    @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="getItemKey(item)"
        class="virtual-list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item" :index="item.index" />
      </div>
    </div>
  </div>
</template>

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

export default {
  name: 'VirtualList',
  
  props: {
    items: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: Number,
      default: 50
    },
    containerHeight: {
      type: Number,
      default: 400
    },
    buffer: {
      type: Number,
      default: 5
    },
    keyField: {
      type: String,
      default: 'id'
    }
  },
  
  setup(props) {
    const containerRef = ref(null)
    const scrollTop = ref(0)
    
    // 计算总高度
    const totalHeight = computed(() => 
      props.items.length * props.itemHeight
    )
    
    // 计算可见区域的起始和结束索引
    const startIndex = computed(() => {
      const index = Math.floor(scrollTop.value / props.itemHeight)
      return Math.max(0, index - props.buffer)
    })
    
    const endIndex = computed(() => {
      const visibleCount = Math.ceil(props.containerHeight / props.itemHeight)
      const index = startIndex.value + visibleCount
      return Math.min(props.items.length - 1, index + props.buffer)
    })
    
    // 计算可见项目
    const visibleItems = computed(() => {
      const items = []
      for (let i = startIndex.value; i <= endIndex.value; i++) {
        if (props.items[i]) {
          items.push({
            ...props.items[i],
            index: i
          })
        }
      }
      return items
    })
    
    // 计算偏移量
    const offsetY = computed(() => 
      startIndex.value * props.itemHeight
    )
    
    // 滚动处理
    function handleScroll(event) {
      scrollTop.value = event.target.scrollTop
    }
    
    // 获取项目键
    function getItemKey(item) {
      return item[props.keyField] || item.index
    }
    
    // 滚动到指定项目
    function scrollToItem(index) {
      if (containerRef.value) {
        const targetScrollTop = index * props.itemHeight
        containerRef.value.scrollTop = targetScrollTop
      }
    }
    
    // 滚动到顶部
    function scrollToTop() {
      scrollToItem(0)
    }
    
    // 滚动到底部
    function scrollToBottom() {
      scrollToItem(props.items.length - 1)
    }
    
    return {
      containerRef,
      totalHeight,
      visibleItems,
      offsetY,
      handleScroll,
      getItemKey,
      scrollToItem,
      scrollToTop,
      scrollToBottom
    }
  }
}
</script>

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

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

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

.virtual-list-item {
  box-sizing: border-box;
}
</style>

图片懒加载组件

<!-- src/components/LazyImage.vue -->
<template>
  <div 
    ref="containerRef"
    class="lazy-image-container"
    :class="{
      'lazy-image-loading': isLoading,
      'lazy-image-loaded': isLoaded,
      'lazy-image-error': hasError
    }"
  >
    <img
      v-if="shouldLoad"
      :src="currentSrc"
      :alt="alt"
      :class="imageClass"
      @load="handleLoad"
      @error="handleError"
    >
    
    <div v-if="isLoading" class="lazy-image-placeholder">
      <slot name="loading">
        <div class="lazy-image-spinner"></div>
      </slot>
    </div>
    
    <div v-if="hasError" class="lazy-image-error-placeholder">
      <slot name="error">
        <div class="lazy-image-error-icon">⚠️</div>
        <p>图片加载失败</p>
      </slot>
    </div>
  </div>
</template>

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

export default {
  name: 'LazyImage',
  
  props: {
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      default: ''
    },
    placeholder: {
      type: String,
      default: ''
    },
    threshold: {
      type: Number,
      default: 0.1
    },
    rootMargin: {
      type: String,
      default: '50px'
    },
    retryCount: {
      type: Number,
      default: 3
    },
    retryDelay: {
      type: Number,
      default: 1000
    }
  },
  
  setup(props) {
    const containerRef = ref(null)
    const isIntersecting = ref(false)
    const isLoading = ref(false)
    const isLoaded = ref(false)
    const hasError = ref(false)
    const currentRetry = ref(0)
    
    let observer = null
    let retryTimer = null
    
    // 是否应该加载图片
    const shouldLoad = computed(() => 
      isIntersecting.value && !hasError.value
    )
    
    // 当前图片源
    const currentSrc = computed(() => {
      if (isLoaded.value) return props.src
      if (isLoading.value && props.placeholder) return props.placeholder
      return props.src
    })
    
    // 图片类名
    const imageClass = computed(() => ({
      'lazy-image': true,
      'lazy-image-fade-in': isLoaded.value
    }))
    
    // 初始化Intersection Observer
    function initObserver() {
      if (!('IntersectionObserver' in window)) {
        // 不支持IntersectionObserver,直接加载
        isIntersecting.value = true
        return
      }
      
      observer = new IntersectionObserver(
        (entries) => {
          const entry = entries[0]
          if (entry.isIntersecting) {
            isIntersecting.value = true
            isLoading.value = true
            observer.unobserve(entry.target)
          }
        },
        {
          threshold: props.threshold,
          rootMargin: props.rootMargin
        }
      )
      
      if (containerRef.value) {
        observer.observe(containerRef.value)
      }
    }
    
    // 图片加载成功
    function handleLoad() {
      isLoading.value = false
      isLoaded.value = true
      hasError.value = false
      currentRetry.value = 0
    }
    
    // 图片加载失败
    function handleError() {
      isLoading.value = false
      
      if (currentRetry.value < props.retryCount) {
        // 重试加载
        currentRetry.value++
        retryTimer = setTimeout(() => {
          isLoading.value = true
          // 强制重新加载
          const img = containerRef.value?.querySelector('img')
          if (img) {
            img.src = props.src + '?retry=' + currentRetry.value
          }
        }, props.retryDelay)
      } else {
        hasError.value = true
      }
    }
    
    // 手动重试
    function retry() {
      if (hasError.value) {
        hasError.value = false
        isLoading.value = true
        currentRetry.value = 0
      }
    }
    
    onMounted(() => {
      initObserver()
    })
    
    onUnmounted(() => {
      if (observer) {
        observer.disconnect()
      }
      if (retryTimer) {
        clearTimeout(retryTimer)
      }
    })
    
    return {
      containerRef,
      shouldLoad,
      currentSrc,
      imageClass,
      isLoading,
      isLoaded,
      hasError,
      handleLoad,
      handleError,
      retry
    }
  }
}
</script>

<style scoped>
.lazy-image-container {
  position: relative;
  display: inline-block;
  overflow: hidden;
}

.lazy-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.3s ease;
}

.lazy-image-fade-in {
  opacity: 1;
}

.lazy-image-loading .lazy-image {
  opacity: 0;
}

.lazy-image-placeholder,
.lazy-image-error-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #f5f5f5;
  color: #666;
}

.lazy-image-spinner {
  width: 2rem;
  height: 2rem;
  border: 2px solid #e0e0e0;
  border-top: 2px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.lazy-image-error-icon {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
</style>

9.3 构建优化

Vite配置优化

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import { compression } from 'vite-plugin-compression'
import { createHtmlPlugin } from 'vite-plugin-html'

export default defineConfig({
  plugins: [
    vue(),
    
    // HTML模板处理
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          title: 'Vue App',
          description: 'A high-performance Vue.js application'
        }
      }
    }),
    
    // Gzip压缩
    compression({
      algorithm: 'gzip',
      ext: '.gz'
    }),
    
    // Brotli压缩
    compression({
      algorithm: 'brotliCompress',
      ext: '.br'
    }),
    
    // 构建分析
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@assets': resolve(__dirname, 'src/assets')
    }
  },
  
  build: {
    // 目标浏览器
    target: 'es2015',
    
    // 输出目录
    outDir: 'dist',
    
    // 静态资源目录
    assetsDir: 'assets',
    
    // 小于此阈值的导入或引用资源将内联为base64编码
    assetsInlineLimit: 4096,
    
    // CSS代码拆分
    cssCodeSplit: true,
    
    // 生成源码映射
    sourcemap: false,
    
    // 构建后是否生成bundle分析报告
    reportCompressedSize: false,
    
    // 消除打包大小超过500kb警告
    chunkSizeWarningLimit: 2000,
    
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html')
      },
      
      output: {
        // 分包策略
        manualChunks: {
          // 将Vue相关库打包到vendor chunk
          vendor: ['vue', 'vue-router', 'pinia'],
          
          // 将UI库单独打包
          ui: ['element-plus', '@element-plus/icons-vue'],
          
          // 将工具库单独打包
          utils: ['lodash-es', 'dayjs', 'axios']
        },
        
        // 文件命名
        chunkFileNames: (chunkInfo) => {
          const facadeModuleId = chunkInfo.facadeModuleId
          if (facadeModuleId) {
            const fileName = facadeModuleId.split('/').pop().replace(/\.[^.]*$/, '')
            return `js/${fileName}-[hash].js`
          }
          return 'js/[name]-[hash].js'
        },
        
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.')
          const ext = info[info.length - 1]
          
          if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/.test(assetInfo.name)) {
            return `media/[name]-[hash].${ext}`
          }
          
          if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name)) {
            return `images/[name]-[hash].${ext}`
          }
          
          if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) {
            return `fonts/[name]-[hash].${ext}`
          }
          
          return `assets/[name]-[hash].${ext}`
        }
      },
      
      // 外部依赖
      external: (id) => {
        // 在生产环境中,可以将某些大型库标记为外部依赖
        // 然后通过CDN加载
        if (process.env.NODE_ENV === 'production') {
          return ['vue', 'vue-router'].includes(id)
        }
        return false
      }
    },
    
    // Terser压缩选项
    terserOptions: {
      compress: {
        // 删除console
        drop_console: true,
        // 删除debugger
        drop_debugger: true,
        // 删除无用代码
        dead_code: true,
        // 删除无用变量
        unused: true
      },
      mangle: {
        // 混淆变量名
        toplevel: true
      }
    }
  },
  
  // 开发服务器配置
  server: {
    host: '0.0.0.0',
    port: 3000,
    open: true,
    cors: true,
    
    // 代理配置
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 预览服务器配置
  preview: {
    host: '0.0.0.0',
    port: 4173,
    open: true
  },
  
  // 依赖优化
  optimizeDeps: {
    include: [
      'vue',
      'vue-router',
      'pinia',
      'axios',
      'lodash-es'
    ],
    exclude: [
      // 排除某些依赖的预构建
    ]
  }
})

代码分割策略

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { performanceMonitor } from '@/utils/performance'

// 路由级别的代码分割
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/Users.vue'),
    children: [
      {
        path: ':id',
        name: 'UserDetail',
        component: () => import('@/views/UserDetail.vue')
      }
    ]
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/admin/Layout.vue'),
    children: [
      {
        path: 'dashboard',
        name: 'AdminDashboard',
        component: () => import('@/views/admin/Dashboard.vue')
      },
      {
        path: 'users',
        name: 'AdminUsers',
        component: () => import('@/views/admin/Users.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// 路由性能监控
router.beforeEach((to, from, next) => {
  // 记录路由开始时间
  to.meta.startTime = performance.now()
  next()
})

router.afterEach((to, from) => {
  // 计算路由耗时
  if (to.meta.startTime) {
    const duration = performance.now() - to.meta.startTime
    performanceMonitor.reportMetrics('route', {
      from: from.name,
      to: to.name,
      duration
    })
  }
})

export default router

组件级别的代码分割

// src/utils/asyncComponent.js
import { defineAsyncComponent, h } from 'vue'
import LoadingComponent from '@/components/LoadingComponent.vue'
import ErrorComponent from '@/components/ErrorComponent.vue'

export function createAsyncComponent(loader, options = {}) {
  return defineAsyncComponent({
    loader,
    
    // 加载组件
    loadingComponent: options.loading || LoadingComponent,
    
    // 错误组件
    errorComponent: options.error || ErrorComponent,
    
    // 显示加载组件前的延迟时间
    delay: options.delay || 200,
    
    // 超时时间
    timeout: options.timeout || 3000,
    
    // 是否可以挂起
    suspensible: options.suspensible !== false,
    
    // 错误处理
    onError(error, retry, fail, attempts) {
      if (attempts <= 3) {
        // 重试3次
        retry()
      } else {
        fail()
      }
    }
  })
}

// 使用示例
export const AsyncChart = createAsyncComponent(
  () => import('@/components/Chart.vue'),
  {
    delay: 300,
    timeout: 5000
  }
)

export const AsyncDataTable = createAsyncComponent(
  () => import('@/components/DataTable.vue')
)

9.4 部署策略

Docker部署

# Dockerfile
# 多阶段构建
FROM node:18-alpine as build-stage

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产阶段
FROM nginx:alpine as production-stage

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 复制构建产物
COPY --from=build-stage /app/dist /usr/share/nginx/html

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;
    
    # 基础配置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    
    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # Brotli压缩(如果支持)
    # brotli on;
    # brotli_comp_level 6;
    # brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;
        
        # 安全头
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; media-src 'self'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self';" always;
        
        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            add_header Vary "Accept-Encoding";
        }
        
        # HTML文件不缓存
        location ~* \.html$ {
            expires -1;
            add_header Cache-Control "no-cache, no-store, must-revalidate";
            add_header Pragma "no-cache";
        }
        
        # API代理
        location /api/ {
            proxy_pass http://backend:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
        
        # SPA路由支持
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # 健康检查
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

CI/CD配置

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linting
      run: npm run lint
    
    - name: Run type checking
      run: npm run type-check
    
    - name: Run tests
      run: npm run test:run
    
    - name: Run E2E tests
      run: npm run test:e2e:headless
  
  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build application
      run: npm run build
      env:
        VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}
        VITE_APP_VERSION: ${{ github.sha }}
    
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: dist
        path: dist/
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Download build artifacts
      uses: actions/download-artifact@v3
      with:
        name: dist
        path: dist/
    
    - name: Build Docker image
      run: |
        docker build -t vue-app:${{ github.sha }} .
        docker tag vue-app:${{ github.sha }} vue-app:latest
    
    - name: Deploy to staging
      run: |
        # 部署到测试环境
        echo "Deploying to staging..."
    
    - name: Run smoke tests
      run: |
        # 运行冒烟测试
        echo "Running smoke tests..."
    
    - name: Deploy to production
      if: success()
      run: |
        # 部署到生产环境
        echo "Deploying to production..."

环境配置

// src/config/index.js
const config = {
  development: {
    apiBaseUrl: 'http://localhost:8080/api',
    enableDevtools: true,
    logLevel: 'debug'
  },
  
  staging: {
    apiBaseUrl: 'https://staging-api.example.com/api',
    enableDevtools: false,
    logLevel: 'info'
  },
  
  production: {
    apiBaseUrl: 'https://api.example.com/api',
    enableDevtools: false,
    logLevel: 'error'
  }
}

const env = import.meta.env.MODE || 'development'

export default {
  ...config[env],
  env,
  version: import.meta.env.VITE_APP_VERSION || 'dev',
  buildTime: import.meta.env.VITE_BUILD_TIME || new Date().toISOString()
}

本章小结

本章我们学习了Vue.js应用的性能优化和部署策略:

  1. 性能分析:使用工具监控和分析应用性能
  2. 代码优化:组件优化、记忆化、虚拟滚动等技巧
  3. 构建优化:Vite配置、代码分割、压缩等策略
  4. 部署策略:Docker、CI/CD、环境配置等实践

下一章预告

下一章我们将学习Vue.js的生态系统和实战项目,包括: - 常用UI组件库的使用 - 实战项目开发 - 最佳实践总结 - 进阶学习路径

练习题

基础练习

  1. 性能监控

    • 实现页面加载时间监控
    • 添加组件渲染性能追踪
    • 创建性能报告面板
  2. 代码优化

    • 优化大列表渲染性能
    • 实现图片懒加载
    • 添加组件缓存机制

进阶练习

  1. 构建优化

    • 配置代码分割策略
    • 优化打包体积
    • 实现渐进式Web应用(PWA)
  2. 部署实践

    • 搭建CI/CD流水线
    • 配置多环境部署
    • 实现蓝绿部署策略

提示:性能优化是一个持续的过程,需要根据实际情况选择合适的优化策略。