You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

数组扁平化导致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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:42:32