1155 lines
23 KiB
Markdown
1155 lines
23 KiB
Markdown
# 健身房管理系统前端性能优化指南
|
||
|
||
> 文档编号: 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 路由懒加载
|
||
|
||
```typescript
|
||
// 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 组件懒加载
|
||
|
||
```vue
|
||
<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 动态导入
|
||
|
||
```typescript
|
||
// 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 图片优化
|
||
|
||
```typescript
|
||
// 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 图片懒加载
|
||
|
||
```vue
|
||
<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 字体优化
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks: {
|
||
'fonts': ['@/assets/fonts']
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 2.3 构建优化
|
||
|
||
#### 2.3.1 代码压缩
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
build: {
|
||
rollupOptions: {
|
||
treeshake: {
|
||
moduleSideEffects: false
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 2.3.3 代码分割策略
|
||
|
||
```typescript
|
||
// 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 预加载与预连接
|
||
|
||
```html
|
||
<!-- 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 虚拟列表组件
|
||
|
||
```vue
|
||
<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 使用虚拟列表
|
||
|
||
```vue
|
||
<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 防抖函数
|
||
|
||
```typescript
|
||
// 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 节流函数
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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 内存缓存
|
||
|
||
```typescript
|
||
// 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 请求缓存
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
import { shallowRef } from 'vue'
|
||
|
||
const memberList = shallowRef<Member[]>([])
|
||
|
||
// 更新数据时创建新数组
|
||
memberList.value = [...memberList.value, newMember]
|
||
```
|
||
|
||
#### 4.1.2 使用computed缓存
|
||
|
||
```typescript
|
||
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
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<h1 v-once>{{ pageTitle }}</h1>
|
||
<p>{{ dynamicContent }}</p>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### 4.2 列表优化
|
||
|
||
#### 4.2.1 使用key
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<member-item
|
||
v-for="member in memberList"
|
||
:key="member.id"
|
||
:member="member"
|
||
/>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
#### 4.2.2 避免v-if和v-for混用
|
||
|
||
```vue
|
||
<!-- 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动画
|
||
|
||
```css
|
||
/* 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
|
||
|
||
```css
|
||
/* 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
|
||
|
||
```css
|
||
.animated-element {
|
||
will-change: transform, opacity;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、实时数据优化
|
||
|
||
### 5.1 WebSocket优化
|
||
|
||
#### 5.1.1 连接管理
|
||
|
||
```typescript
|
||
// 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 数据节流
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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的响应式优化
|
||
|
||
```typescript
|
||
import { shallowRef, triggerRef } from 'vue'
|
||
|
||
const memberList = shallowRef<Member[]>([])
|
||
|
||
// 批量更新
|
||
const updateMembers = (newMembers: Member[]) => {
|
||
memberList.value = newMembers
|
||
triggerRef(memberList)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、性能监控
|
||
|
||
### 6.1 Web Vitals监控
|
||
|
||
```typescript
|
||
// 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 自定义性能监控
|
||
|
||
```typescript
|
||
// 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 错误监控
|
||
|
||
```typescript
|
||
// 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工具
|
||
|
||
通过遵循本文档的性能优化指南,可以确保健身房管理系统前端的高性能,提供优秀的用户体验。
|