TanStack Query (React Query) 架构与性能优化实战:请求去重、缓存分层与数据预取

厘清客户端 UI 状态与服务端缓存的区别,通过 QueryKey 去重、staleTime/gcTime 配置与乐观更新提升数据加载体验。

LJ
李建辉·前端全干工程师

· 9 分钟

本页目录展开 / 收起

读完你能做什么

  • 区分请求函数、服务端状态缓存与客户端 UI 状态。
  • 为 Query Key、staleTime 和 gcTime 建立可预测的规则。
  • 安全实现预取、失效与乐观更新,并在失败时回滚。

Axios 负责“怎样发请求”,TanStack Query 负责“何时请求、结果缓存在哪里、何时视为过期”。两者并不竞争,职责边界清楚后配置才有意义。

在 React 里用 useEffect 发请求,最常见的问题是样板代码满天飞:每个接口都要自己维护 isLoading、isError 和 data 三个状态。组件一多,兄弟组件重复发起相同的接口请求,或者切个 Tab 回来页面又白屏闪一下重新拉数据。TanStack Query 解决的核心痛点,就是把“数据发请求、缓存多久、什么时候后台刷新”当成一套独立的服务端缓存来管。


Axios vs TanStack Query:职责边界划分

TEXT
┌──────────────────────────────────────────────────────────┐
│ TanStack Query (服务端状态中枢)                          │
│  ├── 智能内存缓存 (QueryCache) & 垃圾回收 (gcTime)        │
│  ├── 请求合并与去重 (Request Deduplication)              │
│  ├── 窗口聚焦 / 网络恢复自动静默刷新 (Refetch on Focus)   │
│  └── 乐观更新 (Optimistic UI) & 预取 (Prefetching)        │
├──────────────────────────────────────────────────────────┤
│ Axios / Fetch (底层 HTTP 通信管道)                      │
│  └── 仅负责发送 HTTP 报文、携带 Headers、处理拦截器      │
└──────────────────────────────────────────────────────────┘

[!NOTE] React Query 不是 Axios 的替代品,而是它的管理者。React Query 负责调度“何时发起请求、何时复用缓存”,而具体的数据获取依然通过 Axios 或 fetch 执行。


核心优化一:QueryKey 与并发请求自动去重(Deduplication)

在复杂仪表盘中,导航栏头像组件、侧边栏用户徽章组件和主内容区往往同时需要当前登录用户的信息:

TEXT
组件 A (NavBar)    ─── useQuery(['user', 'me']) ──┐
组件 B (SideBar)   ─── useQuery(['user', 'me']) ──┼──► 仅发出 1 次 HTTP 请求!
组件 C (MainPanel) ─── useQuery(['user', 'me']) ──┘    (其余组件共享同一 In-Flight Promise)

只要 queryKey 严格一致,TanStack Query 会在微任务级别自动拦截并合并所有并发的相同请求,避免后端被无意义的重复流量冲击。


核心优化二:理解 staleTime 与 gcTime

很多开发者对 React Query 数据的“保鲜期”产生误解:

TEXT
数据生命周期时序:
  [数据获取成功] ────(staleTime: 2分钟)────► [数据变为 Stale 陈旧] ────(gcTime: 10分钟)────► [内存回收]
       │                                            │
       ▼                                            ▼
  在此期间重新挂载组件:                      在此期间重新挂载组件:
  直接读取内存缓存,不发起任何网络请求       先展示缓存(0等待),同时后台静默发请求刷新
  • staleTime(数据保鲜期):默认是 0。在 staleTime 内,组件重新渲染或再次挂载直接使用缓存,绝不发请求;
  • gcTime(垃圾回收时间,v4 称 cacheTime):默认是 5 分钟。当没有任何组件在使用这个 query 时,数据会在内存中保留指定时间后被清除。

推荐全局配置:

TS
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
 
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 2, // 2 分钟内数据视为最新,避免频繁抖动
      gcTime: 1000 * 60 * 10,   // 闲置 10 分钟后释放内存
      refetchOnWindowFocus: false, // 视业务需求决定切屏是否刷新
      retry: 2, // 失败重试 2 次
    },
  },
});

核心优化三:乐观更新(Optimistic Updates)提升操作手感

在点赞、收藏或状态切换等高频操作中,等待后端返回 200 再更新 UI 会给用户带来滞后感。

利用 useMutation 的 onMutate,我们可以在请求发出瞬间直接修改本地缓存:

TSX
const queryClient = useQueryClient();
 
const mutation = useMutation({
  mutationFn: (newTodo: { text: string }) => axios.post('/api/todos', newTodo),
  
  // 1. 在请求发送前立即触发
  onMutate: async (newTodo) => {
    // 取消相关的在途查询,防止覆盖乐观数据
    await queryClient.cancelQueries({ queryKey: ['todos'] });
 
    // 保存先前的快照用于回滚
    const previousTodos = queryClient.getQueryData(['todos']);
 
    // 乐观地直接更新本地缓存
    queryClient.setQueryData(['todos'], (old: any) => [...old, { id: 'temp-id', ...newTodo }]);
 
    return { previousTodos };
  },
 
  // 2. 若接口报错,秒级回滚到旧快照
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context?.previousTodos);
    toast.error('添加失败,已回滚');
  },
 
  // 3. 无论成功失败,最后重新与服务端同步一次
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

官方资料

最容易踩的两个坑

  • staleTime 默认值为 0:这是新手最困惑的地方。默认情况下,只要组件重新挂载或窗口切回前台,TanStack Query 就会立刻在后台重新发请求(即使两秒前刚拉过数据)。对于不常变动的基础数据,一定要显式配置合理的 staleTime(例如 1~5 分钟)。
  • QueryKey 数组深度比较:QueryKey 内部采用哈希校验。对象参数中的键值顺序不同也会被判为相同({ a: 1, b: 2 } 与 { b: 2, a: 1 } 视为等价),但传未固定的动态引用或非序列化数据会导致缓存频繁击穿。

评论