Skip to content

前端性能优化实战指南

性能是用户体验的基石。本文将深入探讨前端性能优化的各个方面,分享 Ghuo Design 在性能优化方面的实践经验和技术方案。

性能优化的重要性

用户体验影响

性能直接影响用户体验:

  • 首屏加载时间 - 影响用户第一印象
  • 交互响应速度 - 影响操作流畅度
  • 页面稳定性 - 影响用户信任度
  • 资源消耗 - 影响设备电量和流量

业务价值

良好的性能带来实际的业务收益:

  • 转化率提升 - 加载速度每提升 1 秒,转化率提升 7%
  • 用户留存 - 减少因性能问题导致的用户流失
  • SEO 优化 - 搜索引擎更青睐快速的网站
  • 成本降低 - 减少服务器资源消耗

性能指标体系

Core Web Vitals

Google 提出的核心性能指标:

javascript
// 监控 Core Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

const vitalsReporter = (metric) => {
  // 发送到分析服务
  gtag('event', metric.name, {
    event_category: 'Web Vitals',
    event_label: metric.id,
    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
    non_interaction: true,
  })
}

// 监控各项指标
getCLS(vitalsReporter)  // 累积布局偏移
getFID(vitalsReporter)  // 首次输入延迟
getFCP(vitalsReporter)  // 首次内容绘制
getLCP(vitalsReporter)  // 最大内容绘制
getTTFB(vitalsReporter) // 首字节时间

自定义性能指标

javascript
// 自定义性能监控
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map()
    this.observer = new PerformanceObserver(this.handleEntries.bind(this))
    this.observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] })
  }
  
  // 标记性能时间点
  mark(name) {
    performance.mark(name)
  }
  
  // 测量性能区间
  measure(name, startMark, endMark) {
    performance.measure(name, startMark, endMark)
  }
  
  // 处理性能条目
  handleEntries(list) {
    for (const entry of list.getEntries()) {
      this.processEntry(entry)
    }
  }
  
  // 处理单个性能条目
  processEntry(entry) {
    switch (entry.entryType) {
      case 'measure':
        this.recordCustomMetric(entry)
        break
      case 'navigation':
        this.recordNavigationTiming(entry)
        break
      case 'resource':
        this.recordResourceTiming(entry)
        break
    }
  }
  
  // 记录自定义指标
  recordCustomMetric(entry) {
    this.metrics.set(entry.name, {
      duration: entry.duration,
      startTime: entry.startTime,
      timestamp: Date.now()
    })
  }
}

// 使用示例
const monitor = new PerformanceMonitor()

// 组件渲染性能监控
const ComponentPerformance = {
  beforeMount() {
    monitor.mark(`${this.$options.name}-mount-start`)
  },
  
  mounted() {
    monitor.mark(`${this.$options.name}-mount-end`)
    monitor.measure(
      `${this.$options.name}-mount-duration`,
      `${this.$options.name}-mount-start`,
      `${this.$options.name}-mount-end`
    )
  }
}

加载性能优化

资源优化

代码分割

javascript
// 路由级代码分割
const routes = [
  {
    path: '/home',
    component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
  }
]

// 组件级代码分割
export default {
  components: {
    HeavyComponent: () => import('./HeavyComponent.vue'),
    
    // 条件加载
    AdminPanel: () => {
      if (userRole === 'admin') {
        return import('./AdminPanel.vue')
      }
      return Promise.resolve(null)
    }
  }
}

资源预加载

vue
<template>
  <div>
    <!-- DNS 预解析 -->
    <link rel="dns-prefetch" href="//api.example.com">
    
    <!-- 预连接 -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    
    <!-- 资源预加载 -->
    <link rel="preload" href="/critical.css" as="style">
    <link rel="preload" href="/hero-image.jpg" as="image">
    
    <!-- 预获取下一页资源 -->
    <link rel="prefetch" href="/next-page.js">
  </div>
</template>

<script setup>
// 动态预加载
const preloadRoute = (routeName) => {
  const route = router.resolve({ name: routeName })
  if (route.matched.length > 0) {
    route.matched.forEach(record => {
      if (record.components) {
        Object.values(record.components).forEach(component => {
          if (typeof component === 'function') {
            component() // 触发动态导入
          }
        })
      }
    })
  }
}

// 鼠标悬停时预加载
const handleMouseEnter = (routeName) => {
  preloadRoute(routeName)
}
</script>

图片优化

vue
<template>
  <div>
    <!-- 响应式图片 -->
    <picture>
      <source 
        media="(min-width: 800px)" 
        srcset="/hero-large.webp 1x, /hero-large@2x.webp 2x"
        type="image/webp"
      >
      <source 
        media="(min-width: 400px)" 
        srcset="/hero-medium.webp 1x, /hero-medium@2x.webp 2x"
        type="image/webp"
      >
      <img 
        src="/hero-small.jpg" 
        alt="Hero image"
        loading="lazy"
        decoding="async"
      >
    </picture>
    
    <!-- 懒加载图片组件 -->
    <LazyImage
      :src="imageSrc"
      :placeholder="placeholderSrc"
      :alt="imageAlt"
      @load="handleImageLoad"
      @error="handleImageError"
    />
  </div>
</template>

<script setup>
// 图片懒加载组件
import { ref, onMounted, onUnmounted } from 'vue'

const LazyImage = {
  props: {
    src: String,
    placeholder: String,
    alt: String
  },
  
  setup(props, { emit }) {
    const imageRef = ref()
    const isLoaded = ref(false)
    const isInView = ref(false)
    let observer = null
    
    onMounted(() => {
      // 创建 Intersection Observer
      observer = new IntersectionObserver(
        (entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              isInView.value = true
              observer.unobserve(entry.target)
            }
          })
        },
        { threshold: 0.1 }
      )
      
      if (imageRef.value) {
        observer.observe(imageRef.value)
      }
    })
    
    onUnmounted(() => {
      if (observer) {
        observer.disconnect()
      }
    })
    
    const handleLoad = () => {
      isLoaded.value = true
      emit('load')
    }
    
    const handleError = () => {
      emit('error')
    }
    
    return {
      imageRef,
      isLoaded,
      isInView,
      handleLoad,
      handleError
    }
  },
  
  template: `
    <div ref="imageRef" class="lazy-image">
      <img 
        v-if="isInView"
        :src="src"
        :alt="alt"
        @load="handleLoad"
        @error="handleError"
        :class="{ loaded: isLoaded }"
      >
      <img 
        v-else-if="placeholder"
        :src="placeholder"
        :alt="alt"
        class="placeholder"
      >
    </div>
  `
}
</script>

缓存策略

HTTP 缓存

javascript
// Service Worker 缓存策略
const CACHE_NAME = 'ghuo-design-v1'
const STATIC_CACHE = 'static-v1'
const DYNAMIC_CACHE = 'dynamic-v1'

// 缓存策略配置
const cacheStrategies = {
  // 静态资源:缓存优先
  static: {
    match: /\.(js|css|png|jpg|jpeg|gif|svg|woff2?)$/,
    strategy: 'cacheFirst'
  },
  
  // API 请求:网络优先
  api: {
    match: /\/api\//,
    strategy: 'networkFirst'
  },
  
  // HTML 页面:网络优先,离线时使用缓存
  pages: {
    match: /\.html$/,
    strategy: 'networkFirst'
  }
}

// 缓存策略实现
const cacheFirst = async (request) => {
  const cachedResponse = await caches.match(request)
  if (cachedResponse) {
    return cachedResponse
  }
  
  const networkResponse = await fetch(request)
  const cache = await caches.open(STATIC_CACHE)
  cache.put(request, networkResponse.clone())
  
  return networkResponse
}

const networkFirst = async (request) => {
  try {
    const networkResponse = await fetch(request)
    const cache = await caches.open(DYNAMIC_CACHE)
    cache.put(request, networkResponse.clone())
    return networkResponse
  } catch (error) {
    const cachedResponse = await caches.match(request)
    return cachedResponse || new Response('Offline', { status: 503 })
  }
}

应用级缓存

javascript
// 内存缓存管理
class MemoryCache {
  constructor(maxSize = 100, ttl = 5 * 60 * 1000) {
    this.cache = new Map()
    this.maxSize = maxSize
    this.ttl = ttl
  }
  
  set(key, value) {
    // 清理过期缓存
    this.cleanup()
    
    // 如果超出大小限制,删除最旧的条目
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, {
      value,
      timestamp: Date.now()
    })
  }
  
  get(key) {
    const item = this.cache.get(key)
    if (!item) return null
    
    // 检查是否过期
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key)
      return null
    }
    
    return item.value
  }
  
  cleanup() {
    const now = Date.now()
    for (const [key, item] of this.cache.entries()) {
      if (now - item.timestamp > this.ttl) {
        this.cache.delete(key)
      }
    }
  }
  
  clear() {
    this.cache.clear()
  }
}

// API 缓存装饰器
const apiCache = new MemoryCache()

const withCache = (cacheKey, ttl) => {
  return (target, propertyKey, descriptor) => {
    const originalMethod = descriptor.value
    
    descriptor.value = async function (...args) {
      const key = typeof cacheKey === 'function' 
        ? cacheKey(...args) 
        : `${cacheKey}-${JSON.stringify(args)}`
      
      // 尝试从缓存获取
      const cached = apiCache.get(key)
      if (cached) {
        return cached
      }
      
      // 执行原方法
      const result = await originalMethod.apply(this, args)
      
      // 缓存结果
      apiCache.set(key, result)
      
      return result
    }
  }
}

// 使用示例
class ApiService {
  @withCache('user-list', 5 * 60 * 1000)
  async getUserList() {
    const response = await fetch('/api/users')
    return response.json()
  }
}

运行时性能优化

Vue 3 性能优化

vue
<!-- 组件优化 -->
<template>
  <div>
    <!-- 使用 v-memo 缓存渲染结果 -->
    <div v-memo="[user.id, user.name]">
      <UserCard :user="user" />
    </div>
    
    <!-- 大列表虚拟滚动 -->
    <VirtualList
      :items="largeList"
      :item-height="60"
      :container-height="400"
    >
      <template #item="{ item, index }">
        <ListItem :data="item" :index="index" />
      </template>
    </VirtualList>
    
    <!-- 条件渲染优化 -->
    <KeepAlive :max="10">
      <component :is="currentComponent" />
    </KeepAlive>
  </div>
</template>

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

// 使用 shallowRef 减少响应式开销
const largeData = shallowRef([])

// 使用 markRaw 标记不需要响应式的对象
const chartConfig = markRaw({
  type: 'line',
  options: { /* 大量配置 */ }
})

// 计算属性缓存
const expensiveComputed = computed(() => {
  // 复杂计算逻辑
  return largeData.value.reduce((acc, item) => {
    return acc + item.value
  }, 0)
})

// 防抖处理
import { debounce } from 'lodash-es'

const handleSearch = debounce((query) => {
  // 搜索逻辑
}, 300)
</script>

虚拟滚动实现

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

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

const props = defineProps({
  items: Array,
  itemHeight: Number,
  containerHeight: Number,
  buffer: {
    type: Number,
    default: 5
  }
})

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

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

// 计算可见区域
const visibleRange = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight)
  const end = Math.min(
    start + Math.ceil(props.containerHeight / props.itemHeight),
    props.items.length
  )
  
  return {
    start: Math.max(0, start - props.buffer),
    end: Math.min(props.items.length, end + props.buffer)
  }
})

// 可见项目
const visibleItems = computed(() => {
  const { start, end } = visibleRange.value
  return props.items.slice(start, end).map((item, index) => ({
    data: item,
    index: start + index
  }))
})

// 偏移量
const offsetY = computed(() => {
  return visibleRange.value.start * props.itemHeight
})

// 滚动处理
const handleScroll = (event) => {
  scrollTop.value = event.target.scrollTop
}

// 滚动到指定位置
const scrollToIndex = (index) => {
  const targetScrollTop = index * props.itemHeight
  containerRef.value.scrollTop = targetScrollTop
}

defineExpose({
  scrollToIndex
})
</script>

<style scoped>
.virtual-list {
  overflow-y: auto;
  position: relative;
}

.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;
}
</style>

状态管理优化

javascript
// Pinia store 优化
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useOptimizedStore = defineStore('optimized', () => {
  // 使用 shallowRef 减少深度响应式
  const largeDataset = shallowRef([])
  
  // 分页数据管理
  const currentPage = ref(1)
  const pageSize = ref(20)
  
  // 计算属性缓存
  const paginatedData = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    const end = start + pageSize.value
    return largeDataset.value.slice(start, end)
  })
  
  // 选择性更新
  const updateItem = (id, updates) => {
    const index = largeDataset.value.findIndex(item => item.id === id)
    if (index !== -1) {
      // 只更新变化的字段
      Object.assign(largeDataset.value[index], updates)
    }
  }
  
  // 批量更新
  const batchUpdate = (updates) => {
    // 使用 nextTick 批量更新
    nextTick(() => {
      updates.forEach(({ id, data }) => {
        updateItem(id, data)
      })
    })
  }
  
  return {
    largeDataset,
    currentPage,
    pageSize,
    paginatedData,
    updateItem,
    batchUpdate
  }
})

构建优化

Webpack 优化

javascript
// webpack.config.js
const path = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  // 生产环境优化
  optimization: {
    // 代码分割
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        
        // 公共代码
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5
        },
        
        // UI 组件库
        ui: {
          test: /[\\/]node_modules[\\/](ant-design-vue|element-plus)[\\/]/,
          name: 'ui-lib',
          chunks: 'all',
          priority: 15
        }
      }
    },
    
    // 压缩优化
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    ]
  },
  
  // 模块解析优化
  resolve: {
    // 减少解析步骤
    extensions: ['.js', '.vue', '.json'],
    
    // 别名配置
    alias: {
      '@': path.resolve(__dirname, 'src'),
      'vue$': 'vue/dist/vue.esm-bundler.js'
    },
    
    // 模块查找优化
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules'
    ]
  },
  
  // 构建缓存
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },
  
  plugins: [
    // 分析包大小
    process.env.ANALYZE && new BundleAnalyzerPlugin(),
    
    // 预加载关键资源
    new PreloadWebpackPlugin({
      rel: 'preload',
      include: 'initial'
    })
  ].filter(Boolean)
}

Vite 优化

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

export default defineConfig({
  plugins: [
    vue(),
    
    // 组件自动导入
    AutoImport({
      imports: ['vue', 'vue-router'],
      dts: true
    }),
    
    // 组件自动注册
    Components({
      dts: true
    })
  ],
  
  // 构建优化
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router'],
          'ui-vendor': ['ant-design-vue'],
          'utils-vendor': ['lodash-es', 'dayjs']
        }
      }
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // 资源内联阈值
    assetsInlineLimit: 4096
  },
  
  // 开发服务器优化
  server: {
    // 预构建依赖
    optimizeDeps: {
      include: ['vue', 'vue-router', 'ant-design-vue']
    }
  }
})

监控和分析

性能监控系统

javascript
// 性能监控服务
class PerformanceTracker {
  constructor(config) {
    this.config = config
    this.metrics = []
    this.init()
  }
  
  init() {
    // 监控页面加载性能
    this.trackPageLoad()
    
    // 监控用户交互性能
    this.trackUserInteractions()
    
    // 监控资源加载性能
    this.trackResourceLoad()
    
    // 监控错误
    this.trackErrors()
  }
  
  trackPageLoad() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0]
      
      this.record('page_load', {
        dns_lookup: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp_connect: navigation.connectEnd - navigation.connectStart,
        request: navigation.responseStart - navigation.requestStart,
        response: navigation.responseEnd - navigation.responseStart,
        dom_parse: navigation.domContentLoadedEventEnd - navigation.responseEnd,
        resource_load: navigation.loadEventEnd - navigation.domContentLoadedEventEnd,
        total_time: navigation.loadEventEnd - navigation.navigationStart
      })
    })
  }
  
  trackUserInteractions() {
    // 监控点击响应时间
    document.addEventListener('click', (event) => {
      const startTime = performance.now()
      
      requestAnimationFrame(() => {
        const endTime = performance.now()
        this.record('click_response', {
          duration: endTime - startTime,
          target: event.target.tagName
        })
      })
    })
  }
  
  trackResourceLoad() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          this.record('resource_load', {
            name: entry.name,
            duration: entry.duration,
            size: entry.transferSize,
            type: entry.initiatorType
          })
        }
      }
    })
    
    observer.observe({ entryTypes: ['resource'] })
  }
  
  trackErrors() {
    window.addEventListener('error', (event) => {
      this.record('javascript_error', {
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      })
    })
    
    window.addEventListener('unhandledrejection', (event) => {
      this.record('promise_rejection', {
        reason: event.reason
      })
    })
  }
  
  record(type, data) {
    const metric = {
      type,
      data,
      timestamp: Date.now(),
      url: location.href,
      userAgent: navigator.userAgent
    }
    
    this.metrics.push(metric)
    
    // 批量发送
    if (this.metrics.length >= 10) {
      this.flush()
    }
  }
  
  flush() {
    if (this.metrics.length === 0) return
    
    fetch(this.config.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        metrics: this.metrics,
        session: this.getSessionId()
      })
    }).catch(console.error)
    
    this.metrics = []
  }
  
  getSessionId() {
    let sessionId = sessionStorage.getItem('performance_session_id')
    if (!sessionId) {
      sessionId = Math.random().toString(36).substr(2, 9)
      sessionStorage.setItem('performance_session_id', sessionId)
    }
    return sessionId
  }
}

// 初始化性能监控
const tracker = new PerformanceTracker({
  endpoint: '/api/performance'
})

性能预算

javascript
// 性能预算配置
const performanceBudget = {
  // 资源大小限制
  assets: {
    'bundle.js': 250 * 1024,      // 250KB
    'vendor.js': 500 * 1024,      // 500KB
    'styles.css': 50 * 1024,      // 50KB
    'images/*': 100 * 1024        // 100KB per image
  },
  
  // 性能指标限制
  metrics: {
    FCP: 1500,    // 首次内容绘制 < 1.5s
    LCP: 2500,    // 最大内容绘制 < 2.5s
    FID: 100,     // 首次输入延迟 < 100ms
    CLS: 0.1,     // 累积布局偏移 < 0.1
    TTI: 3500     // 可交互时间 < 3.5s
  }
}

// 性能预算检查
const checkPerformanceBudget = (metrics) => {
  const violations = []
  
  Object.entries(performanceBudget.metrics).forEach(([key, limit]) => {
    if (metrics[key] > limit) {
      violations.push({
        metric: key,
        actual: metrics[key],
        limit: limit,
        severity: metrics[key] > limit * 1.5 ? 'high' : 'medium'
      })
    }
  })
  
  return violations
}

最佳实践总结

加载性能

  1. 资源优化 - 压缩、合并、缓存
  2. 代码分割 - 按需加载,减少初始包大小
  3. 预加载策略 - 预测用户行为,提前加载资源
  4. 缓存策略 - 合理使用各级缓存

运行时性能

  1. 虚拟化 - 大列表使用虚拟滚动
  2. 防抖节流 - 控制高频事件处理
  3. 内存管理 - 及时清理不需要的引用
  4. 响应式优化 - 合理使用 Vue 3 的响应式 API

监控分析

  1. 指标体系 - 建立完整的性能指标体系
  2. 实时监控 - 持续监控性能变化
  3. 性能预算 - 设定性能目标和限制
  4. 数据驱动 - 基于数据进行优化决策

团队协作

  1. 性能文化 - 在团队中建立性能意识
  2. 工具统一 - 使用统一的性能监控工具
  3. 知识共享 - 定期分享性能优化经验
  4. 持续改进 - 建立性能优化的持续改进机制

结语

前端性能优化是一个系统性工程,需要从设计、开发、构建、部署等各个环节进行考虑。通过建立完善的性能监控体系、采用合适的优化策略和工具,我们可以为用户提供更好的体验。

记住,性能优化不是一次性的工作,而是需要持续关注和改进的过程。随着业务的发展和技术的演进,我们需要不断调整和优化我们的性能策略。

最重要的是,性能优化要以用户体验为中心,在技术实现和用户需求之间找到最佳的平衡点。

Released under the MIT License.