Files
martial-web/doc/registration/registration-performance-optimization.md
宅房 5b806e29b7
Some checks failed
continuous-integration/drone/push Build is failing
fix bugs
2025-12-11 16:56:19 +08:00

12 KiB
Raw Permalink Blame History

报名详情页面性能优化

问题描述

用户反馈:点击报名详情页面时出现大批量 API 调用,导致页面加载缓慢。

原问题分析

原实现方式的性能问题

index.vue 页面的 mounted() 钩子中,同时调用了 4 个数据加载方法:

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. 循环调用项目详情 APIN × 3 次)

三个方法都需要查询项目详情,每个方法独立循环调用 getProjectDetail

// 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. 添加缓存数据结构

data() {
  return {
    // ...其他数据
    projectCache: new Map(),  // 项目信息缓存
    participantsCache: null   // 参赛者列表缓存
  }
}

2. 统一的参赛者获取方法(带缓存)

// 统一获取参赛者列表(带缓存)
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. 统一的项目信息获取方法(带缓存)

// 统一的项目信息获取方法(带缓存)
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. 批量预加载项目信息

// 批量预加载项目信<E79BAE><E4BFA1>一次性并行加载所有需要的项目
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() - 预加载所有项目:

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() - 直接使用缓存:

async loadParticipantsStats() {
  const participants = await this.getParticipants()  // 从缓存读取
  // 按单位分组统计...
}

loadProjectTimeStats() - 从缓存读取项目信息:

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() - 从缓存读取价格:

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,250ms3.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. 代码复用:统一的获取方法,避免代码重复

相关文件

修改的文件

修改内容

  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. 后端批量查询接口

创建一个后端批量查询接口:

@PostMapping("/projects/batch")
public R<List<MartialProject>> batchGetProjects(@RequestBody List<Long> projectIds) {
    // 一次性查询多个项目
    List<MartialProject> projects = projectService.listByIds(projectIds);
    return R.data(projects);
}

这样可以将 20 次 getProjectDetail 请求减少到 1 次!

2. 后端聚合查询接口

创建一个后端聚合接口,一次性返回所有统计数据:

@GetMapping("/registration/stats")
public R<RegistrationStatsDTO> getRegistrationStats(@RequestParam Long competitionId) {
    // 后端一次性查询所有需要的数据
    RegistrationStatsDTO stats = registrationService.getStats(competitionId);
    return R.data(stats);
}

这样前端只需要调用 1 个 API 即可获取所有数据!

3. 使用 Vuex 或 Pinia 状态管理

将缓存数据放到全局状态管理中,跨页面共享:

// 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. 优化数据流量 - 批量查询而不是循环单次查询