23 KiB
23 KiB
健身房管理系统前端性能优化指南
文档编号: 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 |
九、总结
本文档详细描述了健身房管理系统前端的性能优化指南,包括:
- 性能目标:性能指标、性能分级
- 加载性能优化:代码分割、资源优化、构建优化、预加载与预连接
- 运行时性能优化:虚拟滚动、防抖与节流、Web Worker、缓存策略
- 渲染性能优化:减少重渲染、列表优化、CSS优化
- 实时数据优化:WebSocket优化、增量更新
- 性能监控:Web Vitals监控、自定义性能监控、错误监控
- 性能优化检查清单:代码层面、资源层面、渲染层面、网络层面、监控层面
- 性能优化工具:开发工具、在线工具、npm工具
通过遵循本文档的性能优化指南,可以确保健身房管理系统前端的高性能,提供优秀的用户体验。