JS脚本多次API调用导致运行耗时过长 优化方案咨询
性能问题诊断
当前耗时过长不是业务场景的必然结果,核心问题是代码的请求逻辑全为串行执行,网络IO等待时间被成倍拉长:
- 前两个分页拉取接口无参数依赖,现有逻辑大概率是等第一个接口全部分页拉取完成后,才启动第二个接口的拉取,总耗时为两个接口耗时之和
- 第三个详情接口在
for循环中逐个await请求,始终保持1个并发,假设单个请求响应耗时200ms,1000个ID就需要200秒等待时间,这部分占了总耗时的90%以上 - 现有代码存在基础逻辑错误:URL中的参数(日期、分页令牌、ID)均为硬编码字符串,没有做变量插值,实际运行无法正确传递参数
具体优化方案
1. 无依赖接口并行执行
前两个分页接口的拉取流程完全独立,不需要互相等待结果,可以并行启动,总耗时等于两个接口中较慢者的耗时,而非两者相加。
可以封装通用分页拉取逻辑避免重复代码:
// 通用全量分页拉取工具 async function fetchAllPages(fetchApi, accessToken, start, end) { const allRecords = [] let nextPageToken = '' do { const res = await fetchApi(accessToken, start, end, nextPageToken) // 按实际接口返回的列表字段调整取值 allRecords.push(...res.data_list) nextPageToken = res.next_page_token } while (nextPageToken) return allRecords } // 并行拉取两个接口的全量分页数据 const [meetingList, chatList] = await Promise.all([ fetchAllPages(getFirstAPI, access_token, start, end), fetchAllPages(getSecondAPI, access_token, start, end) ]) // 提取所有需要查询详情的ID const allTargetIds = [ ...meetingList.map(item => item.id), ...chatList.map(item => item.id) ]
注意分页本身的串行逻辑需要保留,因为下一页的请求必须依赖上一页返回的next_page_token,这部分无法并行。
2. 详情接口添加并发控制,替代串行循环
1并发的串行请求是性能最大瓶颈,但不要无脑发起全量并发,否则很容易触发接口流控(返回429错误),建议根据对接API的流控阈值设置3-10的固定并发数,性能可以直接提升数倍到数十倍。
无需引入第三方依赖的并发控制实现如下:
/** * 并发控制请求工具 * @param {Array} items 待请求的参数列表 * @param {number} concurrency 最大并发数 * @param {Function} requestFn 单条请求逻辑 * @returns {Array} 请求结果,顺序和入参数组一致 */ async function concurrentRequest(items, concurrency, requestFn) { const results = [] let cursor = 0 // 启动指定数量的工作协程 async function worker() { while (cursor < items.length) { const currentIdx = cursor++ const currentParam = items[currentIdx] // 内置错误重试,碰到限流/网络错误最多重试2次 let retryCount = 0 while (retryCount <= 2) { try { results[currentIdx] = await requestFn(currentParam) break } catch (err) { retryCount++ if (retryCount > 2) throw err // 指数退避等待,避免持续触发限流 await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** retryCount)) } } } } await Promise.all(Array.from({ length: concurrency }, () => worker())) return results }
调用时替换原有串行循环:
// 示例设置5个并发,可根据API流控规则调整 const detailList = await concurrentRequest(allTargetIds, 5, async (id) => { // 注意修复原代码的硬编码问题,用模板字符串正确拼接URL参数 const url = `baseAPI/participants/${id}` const resp = await fetch(url, { method: 'GET', headers: { authorization: `Bearer ${access_token}` } }) if (resp.status === 429) throw new Error('rate limit exceeded') return resp.json() }) globalObject.store_data.push(...detailList)
3. 附加优化点
- 优先使用API提供的批量查询接口:如果对接的接口支持一次传入多个ID批量查询详情,直接用批量接口替代单条查询,性能提升会更明显
- 拉取和入库并行:不要等所有数据全部拉完再写入数据库,每拉取到50-100条数据就批量执行一次入库操作,拉取后续数据和入库可以同时进行,进一步压缩总耗时
- 做增量拉取逻辑:已经拉取并存入数据库的数据,后续不需要重复请求,只拉取新增时间范围的数据即可,避免重复请求浪费时间
- 修复原代码的硬编码问题:所有URL中的日期、分页令牌、ID参数都需要通过模板字符串正确插值,否则接口无法返回正确结果
效果预估
按当前单日14分20秒的耗时计算,90%以上的耗时来自串行的详情请求,开启5并发即可将这部分耗时压缩到原来的1/5左右,加上前两个接口并行拉取的优化,单日数据拉取总耗时可以稳定控制在2分钟以内,扩大日期范围也不会出现耗时数小时的情况。
内容的提问来源于stack exchange,提问作者Winterella
相关产品推荐
相关产品推荐

