数组扁平化导致React Query无限列表UI短暂卡顿问题
问题描述
我开发了一款带卡片列表的应用,使用React Query的useInfiniteQuery实现数据无限加载,滚动至列表底部时触发新卡片请求。后端返回基于offset和limit的结构化响应,格式如下:
{ "count": 1546, "next": "http://localhost:8080/api/v1/logs-list/json/?limit=25&offset=150", "previous": "http://localhost:8080/api/v1/logs-list/json/?limit=25&offset=100", "results": [ { "id": 700, "chat": "", "pc_name": "kchernya", "superuser": false, "user_plant": "Plant 1", "ip": "10.249.22.219", "protocol": "[Test-01]", "plant": "Plant 1", "time_create": "2023-12-20T16:42:45.569613+03:00", "if_extra": 0, "extra_str": "", "comment": "131221312321321", "email_list": null, "email_copy_list": null, "status": "Waiting", "attach": true }, { "id": 693, "chat": "", "pc_name": "ykonopat", "superuser": false, "user_plant": "Plant 2", "ip": "10.249.122.120", "protocol": "[Test-01]", "plant": "Plant 2", "time_create": "2023-12-13T15:31:43.395731+03:00", "if_extra": 0, "extra_str": "", "comment": "test", "email_list": null, "email_copy_list": null, "status": "Waiting", "attach": false } // ... ] }
每次新请求响应结构一致,需将返回的results数组合并到已有列表中(扁平化数组)。相关组件代码:
interface LogCardsProps { pageLimit: number; } const LogCards = ({ pageLimit }: LogCardsProps) => { const { data: logs, fetchNextPage, isFetchingNextPage, isLoading, } = useInfiniteLogsQuery(pageLimit); const { ref, inView } = useInView(); const allLogs = useMemo(() => logs?.pages.map((log) => log.results).flat(), [logs]); useEffect(() => { if (inView) fetchNextPage(); }, [fetchNextPage, inView]); return !isLoading ? ( <Box> {allLogs?.map((log) => <LogCard key={log.id} log={log} />)} <Box sx={{ height: '20px' }} ref={ref}> {isFetchingNextPage && <LinearProgress sx={{ m: '2px' }} />} </Box> </Box> ) : ( <CircularProgress /> ); };
请求函数及自定义Hook:
export const fetchLogs = async ( offset: number = 0, pageLimit: number = 25, ): Promise<FetchLogsResponse> => { try { let link = `${BASE_URL}/logs-list/json/?offset=${offset}&limit=${pageLimit}`; const request = await fetch(link); const response = await request.json(); return response; } catch (error) { console.error('Failed to fetch logs:', error); throw error; } };
export const useInfiniteLogsQuery = (pageLimit: number = 25) => useInfiniteQuery({ queryKey: ['logs1'], queryFn: ({ pageParam }) => fetchLogs(pageParam, pageLimit), initialPageParam: 0, getNextPageParam: (lastPage, __, prevLimit) => (lastPage.next ? prevLimit + pageLimit : null), });
目前每次请求完成后,因扁平化所有results数组导致UI短暂卡顿,已使用useMemo优化,仅新卡片重新渲染,但卡顿问题仍存在,求进一步优化方案。
优化方案
1. 提前合并结果,避免重复扁平化
在useInfiniteQuery中使用select选项,直接维护一个合并后的results数组,无需每次在组件中对所有pages执行map+flat操作,减少计算开销:
export const useInfiniteLogsQuery = (pageLimit: number = 25) => useInfiniteQuery({ queryKey: ['logs1'], queryFn: ({ pageParam }) => fetchLogs(pageParam, pageLimit), initialPageParam: 0, getNextPageParam: (lastPage) => { // 从next链接解析offset,比累加计算更可靠 if (!lastPage.next) return null; const urlParams = new URLSearchParams(new URL(lastPage.next).search); return Number(urlParams.get('offset')); }, // 直接在Query阶段合并所有结果 select: (data) => ({ ...data, combinedResults: data.pages.flatMap(page => page.results) }) });
组件中直接使用预合并的结果,省去useMemo计算:
const { data: logs, fetchNextPage, isFetchingNextPage, isLoading, } = useInfiniteLogsQuery(pageLimit); // 直接取合并后的数组 const allLogs = logs?.combinedResults;
2. 虚拟滚动:只渲染可视区域DOM
当列表数据量较大时,渲染全部DOM节点是卡顿的核心原因。使用react-window实现虚拟滚动,仅渲染当前可视区域内的卡片:
首先安装依赖:npm install react-window
修改组件代码:
import { FixedSizeList as List } from 'react-window'; const LogCards = ({ pageLimit }: LogCardsProps) => { const { data: logs, fetchNextPage, isFetchingNextPage, isLoading, } = useInfiniteLogsQuery(pageLimit); const { ref, inView } = useInView(); const allLogs = logs?.combinedResults; useEffect(() => { if (inView) fetchNextPage(); }, [fetchNextPage, inView]); // 虚拟滚动的行渲染组件 const LogCardRow = ({ index, style }) => { const log = allLogs[index]; return <div style={style}><LogCard key={log.id} log={log} /></div>; }; if (isLoading) return <CircularProgress />; return ( <Box> <List height={window.innerHeight - 100} // 根据实际布局调整容器高度 itemCount={allLogs?.length || 0} itemSize={150} // 单个LogCard的固定高度,根据实际情况设置 width="100%" > {LogCardRow} </List> <Box sx={{ height: '20px' }} ref={ref}> {isFetchingNextPage && <LinearProgress sx={{ m: '2px' }} />} </Box> </Box> ); };
3. 优化LogCard组件的渲染稳定性
用React.memo包裹LogCard,确保只有当log props发生变化时才重新渲染:
import React from 'react'; interface LogCardProps { log: typeof allLogs[0]; } const LogCard = React.memo(({ log }: LogCardProps) => { // 原LogCard组件内容 }); export default LogCard;
4. 调整分页触发时机,避免频繁请求
将滚动触发阈值提前,同时添加防抖逻辑,避免因滚动快速触发多次请求:
import { useEffect, useCallback } from 'react'; import { debounce } from 'lodash'; // 或自行实现简单防抖函数 const LogCards = ({ pageLimit }: LogCardsProps) => { const { fetchNextPage } = useInfiniteLogsQuery(pageLimit); // 提前10%可视区域触发请求 const { ref, inView } = useInView({ threshold: 0.1 }); // 防抖处理请求函数 const debouncedFetchNextPage = useCallback(debounce(fetchNextPage, 300), [fetchNextPage]); useEffect(() => { if (inView) debouncedFetchNextPage(); // 组件卸载时取消防抖请求 return () => debouncedFetchNextPage.cancel(); }, [debouncedFetchNextPage, inView]); // 其他代码... };
内容的提问来源于stack exchange,提问作者magrega
相关产品推荐
相关产品推荐

