# 报名详情页面性能优化 ## 问题描述 用户反馈:点击报名详情页面时出现大批量 API 调用,导致页面加载缓慢。 ## 原问题分析 ### 原实现方式的性能问题 在 [index.vue](d:\workspace\31.比赛项目\project\martial-web\src\views\martial\registration\index.vue) 页面的 `mounted()` 钩子中,同时调用了 4 个数据加载方法: ```javascript mounted() { this.competitionId = this.$route.query.competitionId if (this.competitionId) { this.loadCompetitionInfo(this.competitionId) this.loadRegistrationStats() // 方法1 this.loadParticipantsStats() // 方法2 this.loadProjectTimeStats() // 方法3 this.loadAmountStats() // 方法4 } } ``` **存在的严重性能问题**: ### 1. 重复查询参赛者列表(4 次 API 调用) 四个方法都独立调用 `getParticipantList` API: - `loadRegistrationStats()` → 调用 1 次 `getParticipantList` - `loadParticipantsStats()` → 调用 1 次 `getParticipantList` - `loadProjectTimeStats()` → 调用 1 次 `getParticipantList` - `loadAmountStats()` → 调用 1 次 `getParticipantList` **总计:4 次相同的 API 调用**,每次返回几千条数据! ### 2. 循环调用项目详情 API(N × 3 次) 三个方法都需要查询项目详情,每个方法独立循环调用 `getProjectDetail`: ```javascript // loadRegistrationStats() 中 for (const athlete of participants) { const projectId = athlete.projectId || athlete.project_id if (projectId && !projectIds.has(projectId)) { projectIds.add(projectId) const projectRes = await getProjectDetail(projectId) // 第1轮调用 // ... } } // loadProjectTimeStats() 中 for (const [projectId, athleteList] of projectMap) { const projectRes = await getProjectDetail(projectId) // 第2轮调用(重复!) // ... } // loadAmountStats() 中 if (!stat.projectPrices.has(projectId)) { const projectRes = await getProjectDetail(projectId) // 第3轮调用(重复!) // ... } ``` **假设场景**:一个赛事有 20 个不同项目 - `loadRegistrationStats()` 调用 20 次 `getProjectDetail` - `loadProjectTimeStats()` 再调用 20 次 `getProjectDetail` - `loadAmountStats()` 又调用 20 次 `getProjectDetail` **总计:60 次 `getProjectDetail` API 调用!** ### 3. 总体性能开销 对于一个有 **20 个项目、500 名参赛者** 的赛事: | API | 调用次数 | 单次数据量 | 总开销 | |-----|---------|-----------|--------| | `getParticipantList` | **4 次** | 500 条记录 | **2000 条记录传输** | | `getProjectDetail` | **60 次** | 1 条记录 | 60 次网络往返 | | `getCompetitionDetail` | 1 次 | 1 条记录 | 1 次网络往返 | **总计:65 次 API 调用!** 假设每次 API 调用平均耗时 50ms: - 总耗时 = 65 × 50ms = **3.25 秒** - 加上数据处理和渲染 ≈ **4-5 秒** 用户体验极差! ## 优化方案 ### 核心思路:缓存 + 批量加载 1. **缓存参赛者列表**:只调用一次 `getParticipantList`,所有方法共享同一份数据 2. **缓存项目信息**:只调用一次每个项目的 `getProjectDetail`,使用 Map 存储 3. **批量并行加载**:一次性并行加载所有项目信息,而不是串行循环 ### 实现细节 #### 1. 添加缓存数据结构 ```javascript data() { return { // ...其他数据 projectCache: new Map(), // 项目信息缓存 participantsCache: null // 参赛者列表缓存 } } ``` #### 2. 统一的参赛者获取方法(带缓存) ```javascript // 统一获取参赛者列表(带缓存) async getParticipants() { if (this.participantsCache !== null) { return this.participantsCache // 从缓存返回 } try { const res = await getParticipantList(this.competitionId, 1, 10000) const participants = res.data?.data?.records || res.data?.data || [] this.participantsCache = participants // 存入缓存 return participants } catch (err) { console.error('查询参赛者列表失败:', err) return [] } } ``` #### 3. 统一的项目信息获取方法(带缓存) ```javascript // 统一的项目信息获取方法(带缓存) async getProjectInfo(projectId) { if (!projectId) return null // 先从缓存中查找 if (this.projectCache.has(projectId)) { return this.projectCache.get(projectId) } // 缓存中没有,则调用API try { const projectRes = await getProjectDetail(projectId) const projectInfo = projectRes.data?.data if (projectInfo) { this.projectCache.set(projectId, projectInfo) // 存入缓存 return projectInfo } } catch (err) { console.error(`查询项目${projectId}详情失败:`, err) } return null } ``` #### 4. 批量预加载项目信息 ```javascript // 批量预加载项目信��(一次性并行加载所有需要的项目) async preloadProjectInfo(participants) { const projectIds = new Set() participants.forEach(p => { const projectId = p.projectId || p.project_id if (projectId && !this.projectCache.has(projectId)) { projectIds.add(projectId) } }) // 并行加载所有项目信息 if (projectIds.size > 0) { const promises = Array.from(projectIds).map(id => this.getProjectInfo(id)) await Promise.all(promises) // 并行执行,不是串行! } } ``` #### 5. 修改各个加载方法使用缓存 **loadRegistrationStats()** - 预加载所有项目: ```javascript async loadRegistrationStats() { const participants = await this.getParticipants() // 使用缓存 this.competitionInfo.totalParticipants = participants.length // 一次性并行加载所有项目信息 await this.preloadProjectInfo(participants) // 从缓存中获取价格 let totalAmount = 0 const projectIds = new Set() for (const athlete of participants) { const projectId = athlete.projectId || athlete.project_id if (projectId && !projectIds.has(projectId)) { projectIds.add(projectId) const project = this.projectCache.get(projectId) // 从缓存读取 if (project) { totalAmount += parseFloat(project.price || 0) } } } this.competitionInfo.totalAmount = totalAmount.toFixed(2) } ``` **loadParticipantsStats()** - 直接使用缓存: ```javascript async loadParticipantsStats() { const participants = await this.getParticipants() // 从缓存读取 // 按单位分组统计... } ``` **loadProjectTimeStats()** - 从缓存读取项目信息: ```javascript async loadProjectTimeStats() { const participants = await this.getParticipants() // 从缓存读取 // 按项目分组 const projectMap = new Map() participants.forEach(athlete => { // ...分组逻辑 }) // 从缓存中获取项目信息(不再调用API) const projectStats = [] for (const [projectId, athleteList] of projectMap) { const project = this.projectCache.get(projectId) // 从缓存读取 if (project) { projectStats.push({ projectName: project.projectName || project.project_name || '未知项目', // ...其他字段 }) } } this.projectTimeData = projectStats } ``` **loadAmountStats()** - 从缓存读取价格: ```javascript async loadAmountStats() { const participants = await this.getParticipants() // 从缓存读取 const unitMap = new Map() for (const athlete of participants) { const projectId = athlete.projectId || athlete.project_id if (projectId) { stat.projectIds.add(projectId) // 从缓存中获取价格(不再调用API) if (!stat.projectPrices.has(projectId)) { const project = this.projectCache.get(projectId) // 从缓存读取 const price = project ? (project.price || 0) : 0 stat.projectPrices.set(projectId, parseFloat(price)) } } } // ...计算总金额 } ``` ## 优化效果 ### API 调用次数对比 对于一个有 **20 个项目、500 名参赛者** 的赛事: | API | 优化前 | 优化后 | 减少 | |-----|--------|--------|------| | `getParticipantList` | **4 次** | **1 次** | ↓ 75% | | `getProjectDetail` | **60 次** | **20 次(并行)** | ↓ 66.7% | | 总 API 调用 | **65 次** | **21 次** | ↓ 67.7% | ### 性能提升 假设每次 API 调用平均耗时 50ms: **优化前**: - 串行执行:65 × 50ms = **3,250ms(3.25 秒)** - 加上数据处理 ≈ **4-5 秒** **优化后**: - `getParticipantList`: 1 × 50ms = 50ms - `getProjectDetail`: 20 次并行 ≈ 100ms(并行执行,不是串行!) - 内存缓存读取:可忽略不计 - **总耗时 ≈ 150-200ms** - 加上数据处理 ≈ **300-500ms** **性能提升**:从 **4-5 秒** 降低到 **0.3-0.5 秒**,提升约 **90%**! ### 网络流量优化 **优化前**: - 传输 500 条参赛者记录 × 4 次 = **2000 条记录** - 传输 20 条项目记录 × 3 次 = **60 条记录** **优化后**: - 传输 500 条参赛者记录 × 1 次 = **500 条记录** - 传输 20 条项目记录 × 1 次 = **20 条记录** **流量减少约 75%** ## 优化亮点 1. **缓存机制**:避免重复数据获取 2. **并行加载**:`Promise.all` 并行加载项目信息,而不是串行循环 3. **内存优化**:使用 `Map` 数据结构高效存储和查找 4. **代码复用**:统一的获取方法,避免代码重复 ## 相关文件 ### 修改的文件 - [src/views/martial/registration/index.vue](d:\workspace\31.比赛项目\project\martial-web\src\views\martial\registration\index.vue) ### 修改内容 1. 添加缓存数据结构(第 189-190 行) 2. 新增 `getParticipants()` 方法(第 206-221 行) 3. 新增 `getProjectInfo()` 方法(第 223-240 行) 4. 新增 `preloadProjectInfo()` 方法(第 242-255 行) 5. 优化 `loadRegistrationStats()` 方法(第 299-331 行) 6. 优化 `loadParticipantsStats()` 方法(第 333-370 行) 7. 优化 `loadProjectTimeStats()` 方法(第 371-420 行) 8. 优化 `loadAmountStats()` 方法(第 422-472 行) ## 测试验证 ### 如何测试 1. **打开浏览器开发者工具**(F12) 2. **切换到 Network 标签** 3. **点击报名详情页面** 4. **观察网络请求** ### 预期结果 **优化前**: - 看到 4 次 `getParticipantList` 请求 - 看到 60 次 `getProjectDetail` 请求 - 总计 65+ 次请求 **优化后**: - 只看到 1 次 `getParticipantList` 请求 - 只看到 20 次 `getProjectDetail` 请求(并行发起) - 总计 21 次请求 - 页面加载速度明显提升 ## 进一步优化建议 如果还需要继续优化,可以考虑: ### 1. 后端批量查询接口 创建一个后端批量查询接口: ```java @PostMapping("/projects/batch") public R> batchGetProjects(@RequestBody List projectIds) { // 一次性查询多个项目 List projects = projectService.listByIds(projectIds); return R.data(projects); } ``` 这样可以将 20 次 `getProjectDetail` 请求减少到 1 次! ### 2. 后端聚合查询接口 创建一个后端聚合接口,一次性返回所有统计数据: ```java @GetMapping("/registration/stats") public R getRegistrationStats(@RequestParam Long competitionId) { // 后端一次性查询所有需要的数据 RegistrationStatsDTO stats = registrationService.getStats(competitionId); return R.data(stats); } ``` 这样前端只需要调用 1 个 API 即可获取所有数据! ### 3. 使用 Vuex 或 Pinia 状态管理 将缓存数据放到全局状态管理中,跨页面共享: ```javascript // store/modules/competition.js export default { state: { participantsCache: {}, projectCache: {} }, mutations: { SET_PARTICIPANTS_CACHE(state, { competitionId, data }) { state.participantsCache[competitionId] = data } } } ``` ## 总结 通过引入缓存机制和并行加载优化,将报名详情页面的 **65 次 API 调用减少到 21 次**,性能提升约 **90%**,页面加载时间从 **4-5 秒降低到 0.3-0.5 秒**,大幅改善了用户体验。 这是前端性能优化的经典案例,核心原则是: 1. **避免重复请求** - 使用缓存 2. **减少串行等待** - 使用并行加载 3. **优化数据流量** - 批量查询而不是循环单次查询