React useEffect嵌套fetch请求优化:更简洁高效的实现方案
宝可梦图鉴加载性能优化方案
现有代码性能瓶颈
- 无效预加载冗余数据:useEffect依赖
currentOffset,拉完一批自动更新offset触发下一批请求,页面打开后会自动拉完全部1100+宝可梦数据,首屏仅需展示10-20条,提前加载的上百条请求完全浪费带宽和加载时间 - 请求串行阻塞:当前逻辑是等整批10个宝可梦详情全部请求完成后,才开始发起10个物种信息请求,两层请求队列串行等待,总耗时翻倍
- 重复状态存储:
pokemonDetails和filtered存储了完全相同的全量数据,既占用内存,每次更新还会触发额外的重渲染 - 无缓存/错误兜底:重复进入页面会重复拉取所有已加载过的数据,单个请求失败会直接导致整批加载逻辑崩溃
- 并发设置不合理:单批次同时发起20个请求,弱网环境下容易出现请求排队、超时,甚至触发接口限流
具体优化实现
1. 替换自动无限拉取为按需懒加载
放弃自动逐页拉取全量数据的逻辑,配合列表组件的触底回调(如FlatList的onEndReached),仅当用户滑动到列表接近底部时才加载下一页数据,首屏仅加载第一页内容,首屏加载速度可提升10倍以上。
2. 合并请求链路,减少串行等待
把单个宝可梦的「拉详情-拉物种信息-合并数据」拆成独立任务,整批任务统一控制并发,不需要等同批所有宝可梦详情返回再发起物种请求,单条数据链路自己走完全程,总请求耗时可降低40%以上。
3. 精简状态,增加缓存
- 移除重复的
filtered状态,改用useMemo基于全量宝可梦数据实时计算过滤结果 - 增加内存缓存,已拉取过的数据直接读缓存,不重复发请求
- 增加错误兜底,单个宝可梦加载失败不影响整批数据展示
4. 优化后代码示例
import { useEffect, useState, useMemo, useCallback } from 'react'; import { FlatList, Text, ActivityIndicator } from 'react-native'; // 组件外做全局缓存,页面切换不丢失 const pokemonCache = new Map<number, PokemonDetails>(); const PAGE_SIZE = 10; const MAX_POKEMON = 1154; export default function PokemonList() { const [pokemonDetails, setPokemonDetails] = useState<PokemonDetails[]>([]); const [currentOffset, setCurrentOffset] = useState(0); const [isLoading, setIsLoading] = useState(false); const [filterKeyword, setFilterKeyword] = useState(''); const [isEnd, setIsEnd] = useState(false); // 过滤逻辑直接用useMemo计算,不用存两份状态 const filteredList = useMemo(() => { if (!filterKeyword.trim()) return pokemonDetails; const kw = filterKeyword.toLowerCase(); return pokemonDetails.filter(item => item.name.toLowerCase().includes(kw) || item.dex.toString().includes(kw) ); }, [pokemonDetails, filterKeyword]); const loadNextPage = useCallback(async () => { // 正在加载、已经加载完所有数据就直接返回 if (isLoading || isEnd) return; setIsLoading(true); try { // 先拉当前页的宝可梦列表 const listRes = await fetch( `https://pokeapi.co/api/v2/pokemon?limit=${PAGE_SIZE}&offset=${currentOffset}` ); const listData = await listRes.json(); // 单个宝可梦的完整数据拉取逻辑:详情+物种信息合并 const fetchSinglePokemon = async (url: string): Promise<PokemonDetails | null> => { try { // 先读缓存,有就直接返回 const dexMatch = url.match(/pokemon\/(\d+)\//); const dexId = dexMatch ? Number(dexMatch[1]) : 0; if (dexId && pokemonCache.has(dexId)) { return pokemonCache.get(dexId)!; } // 拉详情 const detailRes = await fetch(url); const detail: PokemonDetails = await detailRes.json(); // 拿到物种url直接拉,不用等其他宝可梦 const speciesRes = await fetch(detail.species.url); const speciesInfo: SpeciesInfo = await speciesRes.json(); const fullData = { ...detail, speciesInfo, dex: speciesInfo.id }; // 写入缓存 pokemonCache.set(fullData.dex, fullData); return fullData; } catch (err) { console.log('加载单个宝可梦失败', err); return null; } }; // 批量并发拉取单宝可梦数据,并发设为3-5即可,避免请求堵塞 const fullDataList = (await Promise.map( listData.results, (item: PokemonURLS) => fetchSinglePokemon(item.url), { concurrency: 4 } )).filter(Boolean) as PokemonDetails[]; // 过滤掉加载失败的项 setPokemonDetails(prev => [...prev, ...fullDataList]); const newOffset = currentOffset + PAGE_SIZE; setCurrentOffset(newOffset); if (newOffset >= MAX_POKEMON) setIsEnd(true); } catch (err) { console.log('加载列表失败', err); } finally { setIsLoading(false); } }, [currentOffset, isLoading, isEnd]); // 首屏只加载第一页 useEffect(() => { loadNextPage(); }, []); return ( <FlatList data={filteredList} keyExtractor={item => item.dex.toString()} renderItem={({ item }) => <PokemonCard data={item} />} // 滑到距离底部20%高度时加载下一页 onEndReachedThreshold={0.2} onEndReached={loadNextPage} ListFooterComponent={() => isLoading ? <ActivityIndicator /> : isEnd ? <Text>已经到底啦</Text> : null} /> ); }
额外提速技巧
- 图片加载替换为带缓存的高性能图片组件(如
react-native-fast-image),默认Image组件无缓存、解码慢,会大幅拖慢列表渲染速度 - 请求头增加
Accept-Encoding: gzip,接口返回体积可压缩60%以上 - 不需要等整批10条数据全部加载完再渲染,可以每拿到1条完整数据就更新一次state,用户能更快看到内容,感知加载速度会明显提升
- 物种描述等非首屏必须的信息,可以等用户点进宝可梦详情页再拉,列表页只展示必须的名称、编号、封面图字段,列表加载速度还能再提一倍
内容的提问来源于stack exchange,提问作者Liiaam93
相关产品推荐
相关产品推荐

