Files
gym-manage/docs/design/前端性能优化指南.md
T
2026-03-05 13:48:13 +08:00

23 KiB
Raw Blame History

健身房管理系统前端性能优化指南

文档编号: GYM-FE-PERF-001
版本: v1.0
日期: 2026-03-04
作者: 张翔
状态: 初稿


文档修订历史

版本 日期 作者 修订内容
v1.0 2026-03-04 张翔 创建前端性能优化指南

参考文档

  • 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
  • Web Vitals
  • Google Lighthouse
  • Vue 3 性能优化指南

一、性能目标

1.1 性能指标

指标 目标值 测量工具 重要性
首屏加载时间 (FCP) < 1.8s Lighthouse
最大内容绘制 (LCP) < 2.5s Lighthouse
首次输入延迟 (FID) < 100ms Lighthouse
累积布局偏移 (CLS) < 0.1 Lighthouse
时间到交互 (TTI) < 3.8s Lighthouse
总阻塞时间 (TBT) < 200ms Lighthouse
交互响应时间 < 100ms 自定义监控
API响应时间 < 500ms 自定义监控

1.2 性能分级

级别 FCP LCP FID CLS TTI TBT
优秀 < 1.0s < 1.2s < 50ms < 0.05 < 2.0s < 100ms
良好 1.0-1.8s 1.2-2.5s 50-100ms 0.05-0.1 2.0-3.8s 100-200ms
需改进 > 1.8s > 2.5s > 100ms > 0.1 > 3.8s > 200ms

二、加载性能优化

2.1 代码分割

2.1.1 路由懒加载

// router/index.ts
const routes = [
  {
    path: '/member/list',
    name: 'MemberList',
    component: () => import('@/views/member/List.vue')
  },
  {
    path: '/booking/detail',
    name: 'BookingDetail',
    component: () => import('@/views/booking/Detail.vue')
  }
]

2.1.2 组件懒加载

<template>
  <div>
    <button @click="showChart = true">显示图表</button>
    <lazy-chart v-if="showChart" :data="chartData" />
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'

const LazyChart = defineAsyncComponent(() => import('@/components/business/Chart.vue'))
const showChart = ref(false)
</script>

2.1.3 动态导入

// utils/dynamicImport.ts
export async function loadModule(moduleName: string) {
  const module = await import(`@/modules/${moduleName}/index.ts`)
  return module.default
}

// 使用
const bookingModule = await loadModule('booking')
bookingModule.init()

2.2 资源优化

2.2.1 图片优化

// utils/image.ts
export function getOptimizedImageUrl(url: string, options: ImageOptions): string {
  const params = new URLSearchParams({
    w: options.width?.toString() || '800',
    h: options.height?.toString() || '600',
    q: options.quality?.toString() || '80',
    f: options.format || 'webp'
  })
  return `${url}?${params.toString()}`
}

interface ImageOptions {
  width?: number
  height?: number
  quality?: number
  format?: 'webp' | 'jpeg' | 'png'
}

// 使用
const optimizedUrl = getOptimizedImageUrl(originalUrl, {
  width: 400,
  height: 300,
  quality: 75,
  format: 'webp'
})

2.2.2 图片懒加载

<template>
  <img
    v-lazy="imageUrl"
    :alt="altText"
    loading="lazy"
  />
</template>

<script setup lang="ts">
import { vLazy } from '@/directives/lazy'

const imageUrl = ref('https://example.com/image.jpg')
const altText = ref('图片描述')
</script>

2.2.3 字体优化

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'fonts': ['@/assets/fonts']
        }
      }
    }
  }
})

2.3 构建优化

2.3.1 代码压缩

// vite.config.ts
export default defineConfig({
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log', 'console.info']
      }
    },
    chunkSizeWarningLimit: 1000
  }
})

2.3.2 Tree Shaking

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      treeshake: {
        moduleSideEffects: false
      }
    }
  }
})

2.3.3 代码分割策略

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'element-plus': ['element-plus'],
          'utils': ['lodash-es', 'dayjs'],
          'crypto': ['crypto-js', 'jsencrypt']
        }
      }
    }
  }
})

2.4 预加载与预连接

<!-- index.html -->
<head>
  <!-- DNS预解析 -->
  <link rel="dns-prefetch" href="https://api.example.com">
  <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
  
  <!-- 预连接 -->
  <link rel="preconnect" href="https://api.example.com">
  <link rel="preconnect" href="https://cdn.jsdelivr.net">
  
  <!-- 预加载关键资源 -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/images/logo.png" as="image">
  
  <!-- 预获取可能需要的资源 -->
  <link rel="prefetch" href="/static/large-image.jpg">
</head>

三、运行时性能优化

3.1 虚拟滚动

3.1.1 虚拟列表组件

<template>
  <div class="virtual-list" ref="containerRef">
    <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: `${itemSize}px` }"
      >
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'

interface Props {
  items: any[]
  itemSize: number
  bufferSize?: number
}

const props = withDefaults(defineProps<Props>(), {
  bufferSize: 5
})

const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)

const totalHeight = computed(() => props.items.length * props.itemSize)

const startIndex = computed(() => {
  return Math.max(0, Math.floor(scrollTop.value / props.itemSize) - props.bufferSize)
})

const endIndex = computed(() => {
  return Math.min(
    props.items.length - 1,
    Math.ceil((scrollTop.value + containerHeight.value) / props.itemSize) + props.bufferSize
  )
})

const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, endIndex.value + 1)
})

const offsetY = computed(() => startIndex.value * props.itemSize)

const containerHeight = ref(0)

const handleScroll = () => {
  if (containerRef.value) {
    scrollTop.value = containerRef.value.scrollTop
  }
}

onMounted(() => {
  if (containerRef.value) {
    containerHeight.value = containerRef.value.clientHeight
    containerRef.value.addEventListener('scroll', handleScroll)
  }
})

onUnmounted(() => {
  if (containerRef.value) {
    containerRef.value.removeEventListener('scroll', handleScroll)
  }
})
</script>

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

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

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

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

3.1.2 使用虚拟列表

<template>
  <virtual-list :items="memberList" :item-size="80">
    <template #default="{ item }">
      <member-item :member="item" />
    </template>
  </virtual-list>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import VirtualList from '@/components/base/VirtualList.vue'
import MemberItem from '@/components/business/MemberItem.vue'

const memberList = ref<Member[]>([])
</script>

3.2 防抖与节流

3.2.1 防抖函数

// utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>
  
  return function(this: any, ...args: Parameters<T>) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

// 使用
const handleSearch = debounce((keyword: string) => {
  searchMembers(keyword)
}, 300)

3.2.2 节流函数

// utils/throttle.ts
export function throttle<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let lastTime = 0
  
  return function(this: any, ...args: Parameters<T>) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

// 使用
const handleScroll = throttle(() => {
  loadMoreData()
}, 200)

3.3 Web Worker

3.3.1 创建Worker

// workers/dataProcessor.ts
self.onmessage = (event) => {
  const { data, type } = event.data
  
  if (type === 'process') {
    const result = processData(data)
    self.postMessage({ type: 'result', data: result })
  }
}

function processData(data: any[]): any[] {
  // 复杂数据处理逻辑
  return data.map(item => ({
    ...item,
    processed: true
  }))
}

3.3.2 使用Worker

// utils/worker.ts
export function useWorker<T>(workerScript: string) {
  const worker = ref<Worker | null>(null)
  const result = ref<T | null>(null)
  const loading = ref(false)

  const init = () => {
    worker.value = new Worker(workerScript)
    worker.value.onmessage = (event) => {
      const { type, data } = event.data
      if (type === 'result') {
        result.value = data
        loading.value = false
      }
    }
  }

  const process = (data: any) => {
    if (!worker.value) {
      init()
    }
    loading.value = true
    worker.value?.postMessage({ type: 'process', data })
  }

  const terminate = () => {
    worker.value?.terminate()
    worker.value = null
  }

  return {
    result,
    loading,
    process,
    terminate
  }
}

// 使用
const { result, loading, process, terminate } = useWorker('/workers/dataProcessor.js')
process(largeDataSet)

3.4 缓存策略

3.4.1 内存缓存

// utils/cache.ts
class MemoryCache {
  private cache = new Map<string, { value: any; expireTime: number }>()
  
  set(key: string, value: any, ttl: number = 60000) {
    const expireTime = Date.now() + ttl
    this.cache.set(key, { value, expireTime })
  }
  
  get<T>(key: string): T | null {
    const item = this.cache.get(key)
    if (!item) return null
    
    if (Date.now() > item.expireTime) {
      this.cache.delete(key)
      return null
    }
    
    return item.value as T
  }
  
  has(key: string): boolean {
    return this.cache.has(key)
  }
  
  delete(key: string) {
    this.cache.delete(key)
  }
  
  clear() {
    this.cache.clear()
  }
}

export const memoryCache = new MemoryCache()

3.4.2 请求缓存

// utils/requestCache.ts
import { memoryCache } from './cache'

export function withCache<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number = 60000
): Promise<T> {
  const cached = memoryCache.get<T>(key)
  if (cached) {
    return Promise.resolve(cached)
  }
  
  return fn().then(result => {
    memoryCache.set(key, result, ttl)
    return result
  })
}

// 使用
const memberList = await withCache(
  'member:list:page:1',
  () => api.getMemberList({ page: 1 }),
  30000
)

四、渲染性能优化

4.1 减少重渲染

4.1.1 使用shallowRef

import { shallowRef } from 'vue'

const memberList = shallowRef<Member[]>([])

// 更新数据时创建新数组
memberList.value = [...memberList.value, newMember]

4.1.2 使用computed缓存

import { ref, computed } from 'vue'

const memberList = ref<Member[]>([])
const searchKeyword = ref('')

const filteredMembers = computed(() => {
  return memberList.value.filter(member =>
    member.name.includes(searchKeyword.value)
  )
})

4.1.3 使用v-once

<template>
  <div>
    <h1 v-once>{{ pageTitle }}</h1>
    <p>{{ dynamicContent }}</p>
  </div>
</template>

4.2 列表优化

4.2.1 使用key

<template>
  <div>
    <member-item
      v-for="member in memberList"
      :key="member.id"
      :member="member"
    />
  </div>
</template>

4.2.2 避免v-if和v-for混用

<!-- Bad -->
<template>
  <div>
    <member-item
      v-for="member in memberList"
      v-if="member.active"
      :key="member.id"
      :member="member"
    />
  </div>
</template>

<!-- Good -->
<template>
  <div>
    <member-item
      v-for="member in activeMembers"
      :key="member.id"
      :member="member"
    />
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const activeMembers = computed(() =>
  memberList.value.filter(member => member.active)
)
</script>

4.3 CSS优化

4.3.1 使用CSS动画代替JavaScript动画

/* Good */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* Bad */
.fade-enter-active,
.fade-leave-active {
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

4.3.2 使用transform和opacity

/* Good */
.slide-in {
  transform: translateX(0);
  transition: transform 0.3s ease;
}

.slide-in.from-left {
  transform: translateX(-100%);
}

/* Bad */
.slide-in {
  left: 0;
  transition: left 0.3s ease;
}

.slide-in.from-left {
  left: -100%;
}

4.3.3 使用will-change

.animated-element {
  will-change: transform, opacity;
}

五、实时数据优化

5.1 WebSocket优化

5.1.1 连接管理

// hooks/useWebSocket.ts
import { ref, onUnmounted } from 'vue'

export function useWebSocket(url: string) {
  const ws = ref<WebSocket | null>(null)
  const connected = ref(false)
  const reconnectAttempts = ref(0)
  const maxReconnectAttempts = 5
  const reconnectDelay = 3000

  const connect = () => {
    if (reconnectAttempts.value >= maxReconnectAttempts) {
      console.error('Max reconnect attempts reached')
      return
    }

    ws.value = new WebSocket(url)
    
    ws.value.onopen = () => {
      connected.value = true
      reconnectAttempts.value = 0
      startHeartbeat()
    }
    
    ws.value.onmessage = (event) => {
      handleMessage(event.data)
    }
    
    ws.value.onclose = () => {
      connected.value = false
      reconnect()
    }
    
    ws.value.onerror = (error) => {
      console.error('WebSocket error:', error)
    }
  }

  const disconnect = () => {
    if (ws.value) {
      ws.value.close()
      ws.value = null
    }
  }

  const reconnect = () => {
    reconnectAttempts.value++
    setTimeout(() => {
      connect()
    }, reconnectDelay * reconnectAttempts.value)
  }

  const send = (data: any) => {
    if (ws.value && connected.value) {
      ws.value.send(JSON.stringify(data))
    }
  }

  const startHeartbeat = () => {
    setInterval(() => {
      send({ type: 'ping' })
    }, 30000)
  }

  onUnmounted(() => {
    disconnect()
  })

  return {
    connected,
    connect,
    disconnect,
    send
  }
}

5.1.2 数据节流

// utils/websocketThrottle.ts
export function createWebSocketThrottle(delay: number = 100) {
  let lastUpdateTime = 0
  let pendingUpdate: any = null
  let timer: ReturnType<typeof setTimeout> | null = null

  return {
    update: (data: any, callback: (data: any) => void) => {
      const now = Date.now()
      
      if (now - lastUpdateTime >= delay) {
        lastUpdateTime = now
        callback(data)
      } else {
        pendingUpdate = data
        
        if (timer) {
          clearTimeout(timer)
        }
        
        timer = setTimeout(() => {
          if (pendingUpdate) {
            callback(pendingUpdate)
            pendingUpdate = null
          }
        }, delay - (now - lastUpdateTime))
      }
    }
  }
}

// 使用
const throttle = createWebSocketThrottle(100)

ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  throttle.update(data, (throttledData) => {
    updateState(throttledData)
  })
}

5.2 增量更新

5.2.1 数据diff

// utils/diff.ts
export function diff<T>(oldData: T[], newData: T[], key: keyof T): {
  added: T[]
  removed: T[]
  changed: T[]
} {
  const oldMap = new Map(oldData.map(item => [item[key], item]))
  const newMap = new Map(newData.map(item => [item[key], item]))

  const added: T[] = []
  const removed: T[] = []
  const changed: T[] = []

  for (const [key, newItem] of newMap) {
    const oldItem = oldMap.get(key)
    if (!oldItem) {
      added.push(newItem)
    } else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
      changed.push(newItem)
    }
  }

  for (const [key, oldItem] of oldMap) {
    if (!newMap.has(key)) {
      removed.push(oldItem)
    }
  }

  return { added, removed, changed }
}

// 使用
const { added, removed, changed } = diff(oldMemberList, newMemberList, 'id')

// 增量更新
memberList.value = [
  ...memberList.value.filter(m => !removed.includes(m)),
  ...changed,
  ...added
]

5.2.2 使用Vue的响应式优化

import { shallowRef, triggerRef } from 'vue'

const memberList = shallowRef<Member[]>([])

// 批量更新
const updateMembers = (newMembers: Member[]) => {
  memberList.value = newMembers
  triggerRef(memberList)
}

六、性能监控

6.1 Web Vitals监控

// utils/webVitals.ts
import { onCLS, onFID, onLCP, onTTFB, onFCP } from 'web-vitals'

export function setupWebVitals() {
  onCLS((metric) => {
    console.log('CLS:', metric.value)
    sendToAnalytics('CLS', metric.value)
  })
  
  onFID((metric) => {
    console.log('FID:', metric.value)
    sendToAnalytics('FID', metric.value)
  })
  
  onLCP((metric) => {
    console.log('LCP:', metric.value)
    sendToAnalytics('LCP', metric.value)
  })
  
  onTTFB((metric) => {
    console.log('TTFB:', metric.value)
    sendToAnalytics('TTFB', metric.value)
  })
  
  onFCP((metric) => {
    console.log('FCP:', metric.value)
    sendToAnalytics('FCP', metric.value)
  })
}

function sendToAnalytics(name: string, value: number) {
  // 发送到分析平台
  analytics.track('web-vital', { name, value })
}

6.2 自定义性能监控

// utils/performance.ts
export class PerformanceMonitor {
  private metrics = new Map<string, number>()
  
  startMeasure(name: string) {
    performance.mark(`${name}-start`)
  }
  
  endMeasure(name: string) {
    performance.mark(`${name}-end`)
    performance.measure(name, `${name}-start`, `${name}-end`)
    
    const measure = performance.getEntriesByName(name)[0]
    if (measure) {
      this.metrics.set(name, measure.duration)
      console.log(`${name}: ${measure.duration}ms`)
    }
  }
  
  getMetrics() {
    return Object.fromEntries(this.metrics)
  }
  
  clear() {
    this.metrics.clear()
  }
}

export const performanceMonitor = new PerformanceMonitor()

// 使用
performanceMonitor.startMeasure('api-call')
await api.getMemberList()
performanceMonitor.endMeasure('api-call')

6.3 错误监控

// utils/errorTracking.ts
export function setupErrorTracking() {
  window.addEventListener('error', (event) => {
    console.error('Error:', event.error)
    sendErrorToAnalytics({
      type: 'error',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error?.stack
    })
  })
  
  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled rejection:', event.reason)
    sendErrorToAnalytics({
      type: 'unhandledrejection',
      reason: event.reason
    })
  })
}

function sendErrorToAnalytics(error: any) {
  // 发送到错误监控平台
  analytics.track('error', error)
}

七、性能优化检查清单

7.1 代码层面

  • 使用路由懒加载
  • 使用组件懒加载
  • 使用动态导入
  • 避免不必要的响应式数据
  • 使用computed缓存计算结果
  • 使用shallowRef和shallowReactive
  • 使用v-once优化静态内容
  • 避免v-if和v-for混用
  • 为列表项提供唯一的key
  • 使用防抖和节流优化高频操作

7.2 资源层面

  • 压缩和优化图片
  • 使用WebP格式
  • 实现图片懒加载
  • 使用字体子集
  • 压缩CSS和JavaScript
  • 启用Gzip压缩
  • 使用CDN加速
  • 实现资源预加载
  • 实现DNS预解析
  • 实现预连接

7.3 渲染层面

  • 使用虚拟滚动处理长列表
  • 使用CSS动画代替JavaScript动画
  • 使用transform和opacity实现动画
  • 使用will-change提示浏览器
  • 避免强制同步布局
  • 减少重排和重绘
  • 使用requestAnimationFrame
  • 优化CSS选择器
  • 避免深层嵌套
  • 使用GPU加速

7.4 网络层面

  • 使用HTTP/2
  • 启用HTTPS
  • 配置缓存策略
  • 使用Service Worker
  • 实现离线缓存
  • 优化API请求
  • 使用WebSocket优化实时数据
  • 实现请求节流
  • 使用增量更新
  • 实现数据压缩

7.5 监控层面

  • 配置Web Vitals监控
  • 配置自定义性能监控
  • 配置错误监控
  • 配置API性能监控
  • 配置用户体验监控
  • 设置性能告警
  • 定期性能审计
  • 分析性能瓶颈
  • 持续优化改进
  • 建立性能指标体系

八、性能优化工具

8.1 开发工具

工具 用途 使用场景
Chrome DevTools 性能分析 开发阶段性能调试
Lighthouse 性能评分 性能评估和优化建议
Vue DevTools 组件性能分析 Vue应用性能调试
Webpack Bundle Analyzer 包体积分析 构建优化
vite-plugin-inspect 构建过程分析 Vite构建优化

8.2 在线工具

工具 用途 访问地址
PageSpeed Insights 性能测试 https://pagespeed.web.dev/
WebPageTest 性能测试 https://www.webpagetest.org/
GTmetrix 性能测试 https://gtmetrix.com/
ImageOptim 图片优化 https://imageoptim.com/
TinyPNG 图片压缩 https://tinypng.com/

8.3 npm工具

工具 用途 安装命令
rollup-plugin-visualizer 包体积可视化 npm install rollup-plugin-visualizer
vite-plugin-compression Gzip压缩 npm install vite-plugin-compression
unplugin-vue-components 组件自动导入 npm install unplugin-vue-components
unplugin-auto-import API自动导入 npm install unplugin-auto-import
@vitejs/plugin-legacy 浏览器兼容 npm install @vitejs/plugin-legacy

九、总结

本文档详细描述了健身房管理系统前端的性能优化指南,包括:

  1. 性能目标:性能指标、性能分级
  2. 加载性能优化:代码分割、资源优化、构建优化、预加载与预连接
  3. 运行时性能优化:虚拟滚动、防抖与节流、Web Worker、缓存策略
  4. 渲染性能优化:减少重渲染、列表优化、CSS优化
  5. 实时数据优化WebSocket优化、增量更新
  6. 性能监控:Web Vitals监控、自定义性能监控、错误监控
  7. 性能优化检查清单:代码层面、资源层面、渲染层面、网络层面、监控层面
  8. 性能优化工具:开发工具、在线工具、npm工具

通过遵循本文档的性能优化指南,可以确保健身房管理系统前端的高性能,提供优秀的用户体验。