Skip to content
性能优化

性能优化

TanStack Query 默认已经做了大量性能优化。本章讲解如何利用其内置机制并避免常见的性能陷阱。


内置优化机制

TanStack Query 开箱即用的性能特性:

机制 说明 默认
请求去重 相同查询键同时只发一次请求
缓存共享 多个 observer 共享同一份数据
结构共享(Structural Sharing) 数据引用稳定化——只有实际变化的部分创建新引用
React 18 批量更新 多个状态更新合并为一次重渲染 ✅(需 React 18)
后台重取 stale 数据先返回缓存,后台更新
AbortSignal 取消 组件卸载时自动取消请求

select — 减少重渲染

select 是性能优化的首要工具。它让你只订阅数据的子集:

// ❌ 组件订阅整个 todos 数组 — 任何 todo 变化都重渲染
function TodoCount() {
  const { data: todos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
  return <div>总数:{todos?.length}</div>
}

// ✅ 只订阅数量(原始值)— title 修改不重渲染
function TodoCount() {
  const { data: count } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: (todos) => todos.length,  // 返回原始值
  })
  return <div>总数:{count}</div>
}

select + 引用稳定性

select 返回对象/数组时,只有引用变化才触发重渲染:

// ❌ 每次重取都产生新数组 → 每次都重渲染
select: (todos) => todos.filter(t => t.completed)

// ✅ 提取为稳定引用
const selectCompleted = (todos: Todo[]) => todos.filter(t => t.completed)

// ✅ 配合 queryOptions 工厂
export function doneTodosOptions() {
  return queryOptions({
    queryKey: todoKeys.lists({ status: 'done' }),
    queryFn: fetchDoneTodos,
  })
}
// 然后调用 useQuery(doneTodosOptions())

🔬 深入原理select 使用 useSyncExternalStore 配合 selector 机制。TanStack Query 执行 select 后,将结果与上一次的结果做 Object.is 比较——同值(原始类型)或同引用(对象)则跳过重渲染。


结构共享(Structural Sharing)

结构共享是默认开启的核心优化。它在更新缓存数据时,尽量保持未变化部分的引用不变:

// 假设缓存的 data 是:
const oldData = {
  todos: [
    { id: 1, title: 'A', done: false },
    { id: 2, title: 'B', done: false },
  ]
}

// 服务端返回新数据(只改了第二条的 done):
const newData = {
  todos: [
    { id: 1, title: 'A', done: false },   // ← 未变化,引用保持!
    { id: 2, title: 'B', done: true  },   // ← 变化了,新引用
  ]
}

// 结果:
oldData.todos[0] === newData.todos[0]   // true(引用相等!)
oldData.todos === newData.todos           // false(数组引用变了)

这对 React.memouseMemo 非常关键——未变化的子项不会触发依赖它们的重渲染。

自定义结构共享

// 默认使用 JSON.parse(JSON.stringify()) 做深拷贝
// 对于特殊数据结构(如 Map、Set),需自定义
import { replaceEqualDeep } from '@tanstack/react-query'

useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  structuralSharing: (oldData, newData) => {
    // 返回 newData 的深拷贝,同时保持未变化部分的引用
    return replaceEqualDeep(oldData, newData)
  },
})

禁用结构共享

// 场景:数据永远是全新的(如随机数),不需要结构共享
useQuery({
  queryKey: ['random-number'],
  queryFn: fetchRandomNumber,
  structuralSharing: false,
})

查询键稳定引用

查询键每次渲染都会被重新创建。如果它包含动态生成的对象,会导致不必要的缓存失效:

// ❌ 对象字面量每次渲染都是新引用
useQuery({
  queryKey: ['todos', { status, sort: 'date' }],
  queryFn: fetchTodos,
})
// 虽然值可能相同,但每次都是新对象 → 查询键变化 → 新请求

// ✅ 对频繁变化的场景,使用 useMemo
const queryParams = useMemo(
  () => ({ status, sort: 'date' }),
  [status]
)

useQuery({
  queryKey: ['todos', queryParams],
  queryFn: fetchTodos,
})

⚠️ 实际上,TanStack Query v5 判断查询键相等使用的是深度比较(deep equal),而非引用比较。所以大多数情况下上面的代码不会有问题。但保持键的稳定能减少深度比较的开销,且对 DevTools 中的缓存键可读性有帮助。


queryOptions 工厂模式 — 避免重复创建

// ❌ 每次渲染都创建新的 options 对象
function TodoList() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    staleTime: 30_000,
    select: (todos) => todos.filter(t => !t.completed),
    // 每次渲染都是新对象 + 新 select 函数
  })
}

// ✅ queryOptions 工厂 — 创建一次,到处复用
const todoListOptions = queryOptions({
  queryKey: ['todos'] as const,
  queryFn: fetchTodos,
  staleTime: 30_000,
})

function TodoList() {
  const { data } = useQuery({
    ...todoListOptions,
    select: activeTodosSelector,  // 提取的稳定函数
  })
}

Window Focus 与网络请求优化

默认配置下,用户切回标签页时所有 stale 查询都会重取。大型应用可能产生大量并发请求:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 谨慎关闭:权衡数据新鲜度和带宽
      refetchOnWindowFocus: false,

      // 或者只对特定查询开启
      refetchOnWindowFocus: (query) => {
        // 只有活跃的、重要的查询才重取
        return query.queryKey[0] === 'notifications'
      },
    },
  },
})

性能提示:对于列表/详情这类数据,保持 refetchOnWindowFocus: true 带来的数据新鲜度收益通常大于带宽开销。对配置数据、字典数据等基本不变的内容,设置 staleTime: Infinity 比关闭 refetchOnWindowFocus 更好。


useMemo / useCallback 在 Query 场景中的使用

// queryFn 不需要 useCallback — TanStack Query 只在 queryKey 变化时调用它
// 但 select 需要稳定引用(如果依赖外部变量)
function useFilteredTodos(status: string) {
  const selectFn = useCallback(
    (todos: Todo[]) => todos.filter(t => t.status === status),
    [status]
  )

  return useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    select: selectFn,
  })
}

减少不必要的 data 解构

// ❌ 即使组件不用这些字段,解构也会建立订阅
const { data, isPending, isFetching, error, isStale, isFetched } = useQuery({...})

// ✅ 只取需要的
const { data, isPending, error } = useQuery({...})

🔬 深入原理:TanStack Query 的 useQuery 返回值是一个对象,解构多少字段不影响性能。真正影响性能的是组件如何使用 data——如果整个组件订阅 data 而只有一小部分需要它,应该拆分子组件或用 select


组件拆分策略

// ❌ 一个组件同时处理加载/错误/数据显示
function TodoPage() {
  const { data, isPending, isFetching, error } = useQuery({...})

  if (isPending) return <Spinner />
  if (error) return <Error />
  return (
    <div>
      <Header isFetching={isFetching} />
      <TodoList todos={data} />
    </div>
  )
}

// ✅ 拆分:每个子组件只取自己需要的
function TodoPage() {
  return (
    <div>
      <Suspense fallback={<Spinner />}>
        <TodoHeader />
        <TodoList />
      </Suspense>
    </div>
  )
}

function TodoHeader() {
  const { isFetching } = useQuery({...})   // 只触发重渲染当 isFetching 变化
  return <Header isFetching={isFetching} />
}

function TodoList() {
  const { data } = useQuery({...})
  return <List items={data} />
}

性能诊断工具

React Query Devtools

内置的 DevTools 可以帮助识别性能问题:

  • 查看查询状态(fresh / stale / fetching)
  • 查看 observer 数量(了解哪些组件在使用查询)
  • 手动 invalidate / refetch 测试行为

React DevTools Profiler

  • 检查哪些组件因为 useQuery 数据变化而重渲染
  • 确认 select 是否有效减少了重渲染

常见性能症状与排查

症状 可能原因 排查方向
数据返回后大量组件重渲染 未使用 select / 整个 data 被传递 select 缩减订阅
切换标签页后卡顿 refetchOnWindowFocus 触发大量请求 增加 staleTime 或选择性关闭
内存持续增长 无限查询未设 maxPages 设置 maxPages: 5~10
相同 API 被调用多次 查询键不一致 / 对象字面量键 检查查询键是否相同、使用查询键工厂