12 KiB
报名详情页面性能优化
问题描述
用户反馈:点击报名详情页面时出现大批量 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 次getParticipantListloadParticipantsStats()→ 调用 1 次getParticipantListloadProjectTimeStats()→ 调用 1 次getParticipantListloadAmountStats()→ 调用 1 次getParticipantList
总计:4 次相同的 API 调用,每次返回几千条数据!
2. 循环调用项目详情 API(N × 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 次getProjectDetailloadProjectTimeStats()再调用 20 次getProjectDetailloadAmountStats()又调用 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 秒
用户体验极差!
优化方案
核心思路:缓存 + 批量加载
- 缓存参赛者列表:只调用一次
getParticipantList,所有方法共享同一份数据 - 缓存项目信息:只调用一次每个项目的
getProjectDetail,使用 Map 存储 - 批量并行加载:一次性并行加载所有项目信息,而不是串行循环
实现细节
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,250ms(3.25 秒)
- 加上数据处理 ≈ 4-5 秒
优化后:
getParticipantList: 1 × 50ms = 50msgetProjectDetail: 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%
优化亮点
- 缓存机制:避免重复数据获取
- 并行加载:
Promise.all并行加载项目信息,而不是串行循环 - 内存优化:使用
Map数据结构高效存储和查找 - 代码复用:统一的获取方法,避免代码重复
相关文件
修改的文件
修改内容
- 添加缓存数据结构(第 189-190 行)
- 新增
getParticipants()方法(第 206-221 行) - 新增
getProjectInfo()方法(第 223-240 行) - 新增
preloadProjectInfo()方法(第 242-255 行) - 优化
loadRegistrationStats()方法(第 299-331 行) - 优化
loadParticipantsStats()方法(第 333-370 行) - 优化
loadProjectTimeStats()方法(第 371-420 行) - 优化
loadAmountStats()方法(第 422-472 行)
测试验证
如何测试
- 打开浏览器开发者工具(F12)
- 切换到 Network 标签
- 点击报名详情页面
- 观察网络请求
预期结果
优化前:
- 看到 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 秒,大幅改善了用户体验。
这是前端性能优化的经典案例,核心原则是:
- 避免重复请求 - 使用缓存
- 减少串行等待 - 使用并行加载
- 优化数据流量 - 批量查询而不是循环单次查询