如何重构系列API调用以提升执行速度、缩短页面加载耗时
核心性能问题定位
3-5秒加载耗时的根源不是冗余循环,是代码里的几个设计缺陷:
- 元数据接口完全串行执行:for循环中用await逐个发起请求,总耗时等于所有单个请求耗时的累加。按单个元数据请求平均150ms计算,25个NFT的总请求耗时就达到3.75秒,和你遇到的耗时区间完全吻合。
- 请求流程存在冗余等待:用字符串状态
apiState拆分两个请求步骤,第一次请求完成更新状态后,必须等组件重渲染才能触发第二轮元数据请求,平白增加了空窗期;同时useEffect依赖项缺失apiState、nftData,存在闭包陷阱,可能出现请求不触发、重复触发的异常。 - 无边界处理逻辑:钱包下无资产时
nftData为undefined,直接访问.length会抛错;没有请求失败兜底;无并发控制,资产量大时一次性发几十上百个请求很容易触发接口限流,反而进一步拉长加载时间。 - 额外性能损耗:请求头、请求配置在组件每次渲染时都会重新创建,可能导致useEffect意外重跑,重复发起请求。
优化方案
- 替换串行请求为受控并发请求:通过并发池控制同时发起的请求数量,既把总耗时压缩到批次请求的耗时级别,又避免并发过高触发接口限流。
- 移除多余的中间状态,把两个请求流程合并到同一个异步函数中,消除重渲染等待的空窗期。
- 把固定不变的请求配置移到组件作用域外,避免重复创建触发不必要的effect重跑。
- 增加增量渲染逻辑:每拿到一个NFT的元数据就更新一次视图,不用等所有请求全部返回才展示内容,大幅降低用户感知的加载时长。
- 补全空资产、请求异常的兜底逻辑,解决无资产时的报错问题。
优化后可直接运行的代码
// 固定请求配置移到组件外,仅初始化一次,避免重复创建 const myHeaders = new Headers(); myHeaders.append("X-API-Key", CENTER_API_KEY); const COMMON_REQUEST_OPTIONS = { method: 'GET', headers: myHeaders, redirect: 'follow' }; /** * 并发限流工具 * @param {number} concurrency 最大并发数 * @param {Array<() => Promise<any>>} tasks 异步任务列表 * @returns {Promise<Array<any>>} 所有任务的返回结果 */ async function asyncPool(concurrency, tasks) { const results = []; const executingSet = new Set(); for (const task of tasks) { const promise = Promise.resolve().then(task); results.push(promise); executingSet.add(promise); const cleanTask = () => executingSet.delete(promise); promise.then(cleanTask, cleanTask); if (executingSet.size >= concurrency) { await Promise.race(executingSet); } } return Promise.all(results); } // NFT网格组件 function NftTileGrid() { const [nftList, setNftList] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); useEffect(() => { const fetchAllNftData = async () => { try { setLoading(true); setLoadError(null); setNftList([]); // 拉取钱包下的资产列表 const walletRes = await fetch(walletAPICall, COMMON_REQUEST_OPTIONS); const walletData = await walletRes.json(); const assetList = walletData.items ?? []; // 无资产直接结束流程 if (assetList.length === 0) return; // 构造元数据请求任务,保持原列表顺序 const metaFetchTasks = assetList.map((asset, index) => async () => { const { address, tokenId } = asset; const metaRes = await fetch( `https://api.center.dev/v1/ethereum-mainnet/${address}/${tokenId}`, COMMON_REQUEST_OPTIONS ); const metaData = await metaRes.json(); // 增量渲染:拿到一个元数据就更新一次视图 setNftList(prev => { const next = [...prev]; next[index] = metaData; return next; }); return metaData; }); // 执行并发请求,最大并发数设为6,可根据接口限流规则调整 await asyncPool(6, metaFetchTasks); } catch (err) { setLoadError(err); } finally { setLoading(false); } }; fetchAllNftData(); }, []); // 渲染分支 if (loading) return <div className="loading-tip">资源加载中</div>; if (loadError) return <div className="error-tip">加载失败,请刷新重试</div>; if (nftList.length === 0) return <div className="empty-tip">当前地址下无关联资产</div>; return ( <div className="nft-tile-grid"> {nftList.map(nft => { // 这里写你的图片磁贴渲染逻辑 return <img key={`${nft.address}-${nft.tokenId}`} src={nft.image} alt={nft.name} /> })} </div> ); }
优化效果
以24个NFT、单个请求平均耗时150ms的场景为例:
- 原串行逻辑总请求耗时约3.6秒,优化后并发6个请求的场景下,总请求耗时约600ms,速度提升6倍左右。
- 增量渲染逻辑下,第一个请求返回(约150ms)时用户就能看到第一个图片磁贴,感知加载速度提升更明显。
- 补全了所有边界场景的处理,不会出现无资产时的报错,也不会因为并发太高被接口限流。
- 移除了冗余的状态和重复创建的对象,不会出现意外重复请求的问题。
内容的提问来源于stack exchange,提问作者Cameron
相关产品推荐
相关产品推荐

