渲染性能优化

组件级缓存

// server/cache/component-cache.js
import LRU from 'lru-cache'

class ComponentCache {
  constructor(options = {}) {
    this.cache = new LRU({
      max: options.max || 1000,
      ttl: options.ttl || 1000 * 60 * 15, // 15分钟
      updateAgeOnGet: true
    })
  }
  
  get(key) {
    return this.cache.get(key)
  }
  
  set(key, value, ttl) {
    return this.cache.set(key, value, ttl)
  }
  
  has(key) {
    return this.cache.has(key)
  }
  
  delete(key) {
    return this.cache.delete(key)
  }
  
  clear() {
    return this.cache.clear()
  }
  
  // 生成缓存键
  generateKey(componentName, props = {}, context = {}) {
    const propsKey = JSON.stringify(props)
    const contextKey = JSON.stringify({
      userId: context.userId,
      locale: context.locale
    })
    
    return `${componentName}:${propsKey}:${contextKey}`
  }
}

export const componentCache = new ComponentCache({
  max: 1000,
  ttl: 1000 * 60 * 15 // 15分钟
})

可缓存组件实现

<!-- src/components/CacheableComponent.vue -->
<template>
  <div class="cacheable-component">
    <h3>{{ title }}</h3>
    <div class="content">
      <slot />
    </div>
    <div class="meta">
      <span>缓存时间: {{ cacheTime }}</span>
      <span>渲染时间: {{ renderTime }}ms</span>
    </div>
  </div>
</template>

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

export default {
  name: 'CacheableComponent',
  props: {
    title: String,
    cacheKey: String,
    cacheTTL: {
      type: Number,
      default: 900000 // 15分钟
    }
  },
  
  // 服务端缓存配置
  serverCacheKey: (props, context) => {
    return `cacheable-${props.cacheKey}-${context.userId || 'anonymous'}`
  },
  
  serverCacheTTL: (props) => {
    return props.cacheTTL
  },
  
  setup(props) {
    const cacheTime = ref(new Date().toLocaleString())
    const renderTime = ref(0)
    
    onMounted(() => {
      const startTime = performance.now()
      // 模拟渲染时间
      setTimeout(() => {
        renderTime.value = Math.round(performance.now() - startTime)
      }, 0)
    })
    
    return {
      cacheTime,
      renderTime
    }
  }
}
</script>

页面级缓存

// server/cache/page-cache.js
import { componentCache } from './component-cache.js'
import crypto from 'crypto'

class PageCache {
  constructor() {
    this.cache = new Map()
    this.dependencies = new Map() // 依赖追踪
  }
  
  // 生成页面缓存键
  generatePageKey(url, context = {}) {
    const normalizedUrl = this.normalizeUrl(url)
    const contextHash = this.hashContext(context)
    return `page:${normalizedUrl}:${contextHash}`
  }
  
  // 标准化URL
  normalizeUrl(url) {
    const urlObj = new URL(url, 'http://localhost')
    // 移除不影响内容的查询参数
    urlObj.searchParams.delete('utm_source')
    urlObj.searchParams.delete('utm_medium')
    urlObj.searchParams.delete('utm_campaign')
    return urlObj.pathname + urlObj.search
  }
  
  // 生成上下文哈希
  hashContext(context) {
    const relevantContext = {
      userId: context.userId,
      userRole: context.userRole,
      locale: context.locale,
      theme: context.theme
    }
    
    return crypto
      .createHash('md5')
      .update(JSON.stringify(relevantContext))
      .digest('hex')
      .substring(0, 8)
  }
  
  // 获取缓存
  get(url, context) {
    const key = this.generatePageKey(url, context)
    const cached = this.cache.get(key)
    
    if (cached && cached.expires > Date.now()) {
      return cached.html
    }
    
    // 清理过期缓存
    if (cached) {
      this.cache.delete(key)
    }
    
    return null
  }
  
  // 设置缓存
  set(url, context, html, ttl = 300000) { // 默认5分钟
    const key = this.generatePageKey(url, context)
    
    this.cache.set(key, {
      html,
      expires: Date.now() + ttl,
      createdAt: Date.now()
    })
    
    // 设置依赖
    this.setDependencies(key, context)
  }
  
  // 设置缓存依赖
  setDependencies(cacheKey, context) {
    const deps = [
      `user:${context.userId}`,
      `locale:${context.locale}`
    ]
    
    deps.forEach(dep => {
      if (!this.dependencies.has(dep)) {
        this.dependencies.set(dep, new Set())
      }
      this.dependencies.get(dep).add(cacheKey)
    })
  }
  
  // 根据依赖清理缓存
  invalidateByDependency(dependency) {
    const cacheKeys = this.dependencies.get(dependency)
    if (cacheKeys) {
      cacheKeys.forEach(key => {
        this.cache.delete(key)
      })
      this.dependencies.delete(dependency)
    }
  }
  
  // 清理所有缓存
  clear() {
    this.cache.clear()
    this.dependencies.clear()
  }
  
  // 获取缓存统计
  getStats() {
    return {
      size: this.cache.size,
      dependencies: this.dependencies.size,
      memory: this.getMemoryUsage()
    }
  }
  
  getMemoryUsage() {
    let totalSize = 0
    this.cache.forEach(value => {
      totalSize += Buffer.byteLength(value.html, 'utf8')
    })
    return totalSize
  }
}

export const pageCache = new PageCache()

数据缓存策略

Redis缓存实现

// server/cache/redis-cache.js
import Redis from 'ioredis'

class RedisCache {
  constructor(options = {}) {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD,
      db: process.env.REDIS_DB || 0,
      retryDelayOnFailover: 100,
      maxRetriesPerRequest: 3,
      ...options
    })
    
    this.redis.on('error', (err) => {
      console.error('Redis连接错误:', err)
    })
    
    this.redis.on('connect', () => {
      console.log('Redis连接成功')
    })
  }
  
  // 获取缓存
  async get(key) {
    try {
      const value = await this.redis.get(key)
      return value ? JSON.parse(value) : null
    } catch (error) {
      console.error('Redis获取缓存失败:', error)
      return null
    }
  }
  
  // 设置缓存
  async set(key, value, ttl = 3600) {
    try {
      const serialized = JSON.stringify(value)
      if (ttl > 0) {
        await this.redis.setex(key, ttl, serialized)
      } else {
        await this.redis.set(key, serialized)
      }
      return true
    } catch (error) {
      console.error('Redis设置缓存失败:', error)
      return false
    }
  }
  
  // 删除缓存
  async del(key) {
    try {
      await this.redis.del(key)
      return true
    } catch (error) {
      console.error('Redis删除缓存失败:', error)
      return false
    }
  }
  
  // 批量删除
  async delPattern(pattern) {
    try {
      const keys = await this.redis.keys(pattern)
      if (keys.length > 0) {
        await this.redis.del(...keys)
      }
      return keys.length
    } catch (error) {
      console.error('Redis批量删除失败:', error)
      return 0
    }
  }
  
  // 检查键是否存在
  async exists(key) {
    try {
      return await this.redis.exists(key) === 1
    } catch (error) {
      console.error('Redis检查键存在失败:', error)
      return false
    }
  }
  
  // 设置过期时间
  async expire(key, ttl) {
    try {
      await this.redis.expire(key, ttl)
      return true
    } catch (error) {
      console.error('Redis设置过期时间失败:', error)
      return false
    }
  }
  
  // 获取剩余过期时间
  async ttl(key) {
    try {
      return await this.redis.ttl(key)
    } catch (error) {
      console.error('Redis获取TTL失败:', error)
      return -1
    }
  }
  
  // 关闭连接
  async close() {
    await this.redis.quit()
  }
}

export const redisCache = new RedisCache()

多级缓存策略

// server/cache/multi-level-cache.js
import { componentCache } from './component-cache.js'
import { redisCache } from './redis-cache.js'

class MultiLevelCache {
  constructor() {
    this.l1Cache = componentCache // 内存缓存(L1)
    this.l2Cache = redisCache     // Redis缓存(L2)
  }
  
  // 获取缓存(先查L1,再查L2)
  async get(key) {
    // 先查内存缓存
    let value = this.l1Cache.get(key)
    if (value !== undefined) {
      return value
    }
    
    // 再查Redis缓存
    value = await this.l2Cache.get(key)
    if (value !== null) {
      // 回填到内存缓存
      this.l1Cache.set(key, value, 300000) // 5分钟
      return value
    }
    
    return null
  }
  
  // 设置缓存(同时设置L1和L2)
  async set(key, value, ttl = 3600) {
    // 设置内存缓存
    this.l1Cache.set(key, value, Math.min(ttl * 1000, 300000))
    
    // 设置Redis缓存
    await this.l2Cache.set(key, value, ttl)
  }
  
  // 删除缓存(同时删除L1和L2)
  async del(key) {
    this.l1Cache.delete(key)
    await this.l2Cache.del(key)
  }
  
  // 清空所有缓存
  async clear() {
    this.l1Cache.clear()
    await this.l2Cache.delPattern('*')
  }
  
  // 预热缓存
  async warmup(keys) {
    const promises = keys.map(async (key) => {
      const value = await this.l2Cache.get(key)
      if (value !== null) {
        this.l1Cache.set(key, value, 300000)
      }
    })
    
    await Promise.all(promises)
  }
}

export const multiLevelCache = new MultiLevelCache()

代码分割优化

路由级代码分割

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

// 首页同步加载,提高首屏速度
import Home from '@/pages/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(
      /* webpackChunkName: "about" */
      /* webpackPreload: true */
      '@/pages/About.vue'
    )
  },
  {
    path: '/blog',
    name: 'Blog',
    component: () => import(
      /* webpackChunkName: "blog" */
      '@/pages/Blog.vue'
    ),
    children: [
      {
        path: ':id',
        name: 'BlogPost',
        component: () => import(
          /* webpackChunkName: "blog-post" */
          '@/pages/BlogPost.vue'
        )
      }
    ]
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import(
      /* webpackChunkName: "admin" */
      '@/pages/Admin.vue'
    ),
    meta: { requiresAuth: true }
  }
]

export function createRouter() {
  return createRouter({
    history: import.meta.env.SSR 
      ? createMemoryHistory() 
      : createWebHistory(),
    routes
  })
}

组件级代码分割

<!-- src/pages/Dashboard.vue -->
<template>
  <div class="dashboard">
    <h1>仪表板</h1>
    
    <!-- 关键组件同步加载 -->
    <UserInfo :user="user" />
    
    <!-- 非关键组件异步加载 -->
    <Suspense>
      <template #default>
        <AsyncChart :data="chartData" />
      </template>
      <template #fallback>
        <ChartSkeleton />
      </template>
    </Suspense>
    
    <!-- 条件加载的组件 -->
    <Suspense v-if="showAdvanced">
      <template #default>
        <AdvancedAnalytics />
      </template>
      <template #fallback>
        <div>加载高级分析...</div>
      </template>
    </Suspense>
  </div>
</template>

<script>
import { ref, defineAsyncComponent } from 'vue'
import UserInfo from '@/components/UserInfo.vue'
import ChartSkeleton from '@/components/ChartSkeleton.vue'

export default {
  name: 'Dashboard',
  
  components: {
    UserInfo,
    ChartSkeleton,
    
    // 异步组件
    AsyncChart: defineAsyncComponent({
      loader: () => import('@/components/Chart.vue'),
      delay: 200,
      timeout: 3000,
      errorComponent: () => import('@/components/ChartError.vue'),
      loadingComponent: ChartSkeleton
    }),
    
    AdvancedAnalytics: defineAsyncComponent(() => 
      import(
        /* webpackChunkName: "advanced-analytics" */
        '@/components/AdvancedAnalytics.vue'
      )
    )
  },
  
  setup() {
    const user = ref({})
    const chartData = ref([])
    const showAdvanced = ref(false)
    
    return {
      user,
      chartData,
      showAdvanced
    }
  }
}
</script>

资源优化

图片懒加载组件

<!-- src/components/LazyImage.vue -->
<template>
  <div class="lazy-image" :class="{ loaded: isLoaded }">
    <img
      v-if="shouldLoad"
      :src="src"
      :alt="alt"
      @load="onLoad"
      @error="onError"
      :loading="loading"
    />
    <div v-else class="placeholder">
      <slot name="placeholder">
        <div class="skeleton"></div>
      </slot>
    </div>
    
    <div v-if="error" class="error">
      <slot name="error">
        <span>图片加载失败</span>
      </slot>
    </div>
  </div>
</template>

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

export default {
  name: 'LazyImage',
  props: {
    src: {
      type: String,
      required: true
    },
    alt: {
      type: String,
      default: ''
    },
    loading: {
      type: String,
      default: 'lazy'
    },
    threshold: {
      type: Number,
      default: 0.1
    }
  },
  
  setup(props, { emit }) {
    const shouldLoad = ref(false)
    const isLoaded = ref(false)
    const error = ref(false)
    const imageRef = ref(null)
    let observer = null
    
    const onLoad = () => {
      isLoaded.value = true
      emit('load')
    }
    
    const onError = () => {
      error.value = true
      emit('error')
    }
    
    onMounted(() => {
      if ('IntersectionObserver' in window) {
        observer = new IntersectionObserver(
          (entries) => {
            entries.forEach(entry => {
              if (entry.isIntersecting) {
                shouldLoad.value = true
                observer.unobserve(entry.target)
              }
            })
          },
          {
            threshold: props.threshold
          }
        )
        
        if (imageRef.value) {
          observer.observe(imageRef.value)
        }
      } else {
        // 不支持IntersectionObserver时直接加载
        shouldLoad.value = true
      }
    })
    
    onUnmounted(() => {
      if (observer) {
        observer.disconnect()
      }
    })
    
    return {
      shouldLoad,
      isLoaded,
      error,
      imageRef,
      onLoad,
      onError
    }
  }
}
</script>

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

.placeholder {
  width: 100%;
  height: 200px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.skeleton {
  width: 100%;
  height: 100%;
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

.error {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #ff6b6b;
}

img {
  width: 100%;
  height: auto;
  transition: opacity 0.3s ease;
}

.loaded img {
  opacity: 1;
}
</style>

资源预加载策略

// src/utils/preloader.js
class ResourcePreloader {
  constructor() {
    this.preloadedResources = new Set()
    this.preloadQueue = []
    this.isPreloading = false
  }
  
  // 预加载图片
  preloadImage(src) {
    return new Promise((resolve, reject) => {
      if (this.preloadedResources.has(src)) {
        resolve(src)
        return
      }
      
      const img = new Image()
      img.onload = () => {
        this.preloadedResources.add(src)
        resolve(src)
      }
      img.onerror = reject
      img.src = src
    })
  }
  
  // 预加载多个图片
  async preloadImages(srcs) {
    const promises = srcs.map(src => this.preloadImage(src))
    return Promise.allSettled(promises)
  }
  
  // 预加载JavaScript模块
  preloadModule(moduleFactory) {
    return new Promise((resolve, reject) => {
      if (typeof moduleFactory === 'function') {
        moduleFactory()
          .then(module => {
            resolve(module)
          })
          .catch(reject)
      } else {
        reject(new Error('Invalid module factory'))
      }
    })
  }
  
  // 智能预加载(基于用户行为)
  smartPreload(resources, priority = 'low') {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        this.batchPreload(resources)
      })
    } else {
      setTimeout(() => {
        this.batchPreload(resources)
      }, priority === 'high' ? 0 : 1000)
    }
  }
  
  // 批量预加载
  async batchPreload(resources) {
    if (this.isPreloading) return
    
    this.isPreloading = true
    
    try {
      for (const resource of resources) {
        if (resource.type === 'image') {
          await this.preloadImage(resource.src)
        } else if (resource.type === 'module') {
          await this.preloadModule(resource.factory)
        }
        
        // 避免阻塞主线程
        await new Promise(resolve => setTimeout(resolve, 10))
      }
    } finally {
      this.isPreloading = false
    }
  }
  
  // 清理预加载缓存
  clearCache() {
    this.preloadedResources.clear()
  }
}

export const preloader = new ResourcePreloader()

// 使用示例
export function usePreloader() {
  const preloadRouteResources = (routeName) => {
    const routeResources = {
      'Blog': [
        { type: 'image', src: '/images/blog-hero.jpg' },
        { type: 'module', factory: () => import('@/components/BlogList.vue') }
      ],
      'Admin': [
        { type: 'module', factory: () => import('@/components/AdminDashboard.vue') },
        { type: 'module', factory: () => import('@/components/UserManagement.vue') }
      ]
    }
    
    const resources = routeResources[routeName]
    if (resources) {
      preloader.smartPreload(resources)
    }
  }
  
  return {
    preloadRouteResources,
    preloader
  }
}

监控和分析

性能监控

// src/utils/performance-monitor.js
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map()
    this.observers = []
    this.init()
  }
  
  init() {
    // 监控页面加载性能
    if ('PerformanceObserver' in window) {
      this.observeNavigation()
      this.observePaint()
      this.observeLCP()
      this.observeFID()
      this.observeCLS()
    }
  }
  
  // 监控导航时间
  observeNavigation() {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.metrics.set('navigation', {
          domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
          loadComplete: entry.loadEventEnd - entry.loadEventStart,
          firstByte: entry.responseStart - entry.requestStart,
          domInteractive: entry.domInteractive - entry.navigationStart
        })
      })
    })
    
    observer.observe({ entryTypes: ['navigation'] })
    this.observers.push(observer)
  }
  
  // 监控绘制时间
  observePaint() {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.metrics.set(entry.name, entry.startTime)
      })
    })
    
    observer.observe({ entryTypes: ['paint'] })
    this.observers.push(observer)
  }
  
  // 监控最大内容绘制(LCP)
  observeLCP() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      this.metrics.set('lcp', lastEntry.startTime)
    })
    
    observer.observe({ entryTypes: ['largest-contentful-paint'] })
    this.observers.push(observer)
  }
  
  // 监控首次输入延迟(FID)
  observeFID() {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        this.metrics.set('fid', entry.processingStart - entry.startTime)
      })
    })
    
    observer.observe({ entryTypes: ['first-input'] })
    this.observers.push(observer)
  }
  
  // 监控累积布局偏移(CLS)
  observeCLS() {
    let clsValue = 0
    
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
          this.metrics.set('cls', clsValue)
        }
      })
    })
    
    observer.observe({ entryTypes: ['layout-shift'] })
    this.observers.push(observer)
  }
  
  // 获取所有指标
  getMetrics() {
    return Object.fromEntries(this.metrics)
  }
  
  // 发送指标到分析服务
  async sendMetrics() {
    const metrics = this.getMetrics()
    
    try {
      await fetch('/api/analytics/performance', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          url: window.location.href,
          userAgent: navigator.userAgent,
          timestamp: Date.now(),
          metrics
        })
      })
    } catch (error) {
      console.error('发送性能指标失败:', error)
    }
  }
  
  // 清理观察器
  disconnect() {
    this.observers.forEach(observer => observer.disconnect())
    this.observers = []
  }
}

export const performanceMonitor = new PerformanceMonitor()

// 页面卸载时发送指标
if (typeof window !== 'undefined') {
  window.addEventListener('beforeunload', () => {
    performanceMonitor.sendMetrics()
  })
}

下一步

在下一章节中,我们将学习如何部署Vue SSR应用到生产环境,包括Docker容器化、CI/CD流程等。