缓存机制:staleTime 与 gcTime
本教程共 38 篇 · 第 8 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
8. 缓存机制:staleTime 与 gcTime
本节目标:彻底搞懂 Query 的缓存生命周期,分清 staleTime 和 gcTime 的区别,理解缓存共享、窗口聚焦重取等默认行为,学完能精准控制缓存策略。
8.1 先理清两个时间概念
这两个概念是 Query 缓存的核心,也是新手最容易混的。v5 把 cacheTime 改名成了 gcTime,就是因为大家老理解错。
打个比方。缓存里的数据像冰箱里的食物:
- staleTime 是「保质期」。食物在保质期内是新鲜的,过了保质期算过期(stale),但不代表马上扔。过期了还能吃,只是会考虑重新买一份新鲜的。
- gcTime 是「清理时间」。食物从冰箱拿出来不用了,过这么久还没人管,就扔掉腾地方。
数据有组件在用(active)时,gcTime 不起作用。只有数据没人用了(inactive),才开始倒计时 gcTime,时间到了就垃圾回收。
8.2 staleTime:数据新鲜期
默认 staleTime 是 0,意思是数据一拿到就立刻算过期。这听起来很激进,但背后的逻辑是:服务端数据随时可能变,默认保守一点。
staleTime 影响什么?过期(stale)的数据会在特定时机自动重新请求:
- 新的组件实例挂载(
refetchOnMount) - 窗口重新获得焦点(
refetchOnWindowFocus) - 网络重新连接(
refetchOnReconnect)
如果数据是 fresh 的(还在 staleTime 内),上面这些时机不会触发重取。
改 staleTime:
// 全局
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 2, // 2 分钟内不重取
},
},
})
// 单个查询
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 2, // 2 分钟
})
几个特殊值:
staleTime: 0— 默认值,数据立即过期staleTime: Infinity— 永不过期,除非手动失效staleTime: 'static'— 永不过期,连手动失效都无效
Note
'static'比Infinity更严格。Infinity还能被invalidateQueries失效,'static'连失效都不吃。适合那种「应用运行期间绝不变」的数据,比如启动时加载的功能开关、用户权限。
8.3 gcTime:垃圾回收时间
v5 把 cacheTime 改名 gcTime,因为原名误导。cacheTime 听着像「数据缓存多久」,但实际上它只在数据没人用时才生效。
默认 gcTime 是 5 分钟。意思是:一个查询的所有组件都卸载了,5 分钟内没人再用,缓存数据就被清理掉。
// 全局
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 10, // 10 分钟后回收
},
},
})
// 单个查询
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 10,
})
为什么要等 5 分钟才删?因为用户可能马上切回来。保留一会儿,切回来时能立即显示缓存数据,同时后台请求新数据,体验更顺滑。
8.4 缓存的完整生命周期
把两者串起来看一个完整的生命周期。假设默认配置(staleTime 0,gcTime 5 分钟):
-
组件 A 挂载,发起
useQuery({ queryKey: ['todos'] })- 缓存里没数据,显示 loading,发请求
- 请求完成,数据进缓存,标记为 stale(因为 staleTime 0)
-
组件 B 也挂载,用同样的 key
- 缓存有数据,立即返回给 B(不显示 loading)
- 同时后台发新请求(因为数据已经 stale)
- 新请求完成,A 和 B 都更新成新数据
-
A 和 B 都卸载,没人用这个查询了
- 查询变成 inactive 状态
- gcTime 开始倒计时 5 分钟
-
3 分钟后,组件 C 挂载用同样的 key
- gcTime 还没到,缓存还在
- 立即返回缓存数据,同时后台请求
- gcTime 计时取消(又有 active 了)
-
C 也卸载,5 分钟内没人再用
- 缓存数据被清理,彻底消失
Tip第 2 步是「缓存共享」的精髓:多个组件用同一个 key,只发一个请求,数据共享。这就是为什么 Query 能大幅减少重复请求。
8.5 缓存共享机制
同一个 queryKey 的查询会共享缓存。不管多少个组件用 ['todos'],底层只有一个缓存条目、一个请求。
// 组件 A
function ComponentA() {
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ...
}
// 组件 B,另一处
function ComponentB() {
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ...
}
A 先挂载发请求,B 挂载时直接拿缓存数据,同时可能触发后台刷新。两个组件看到的是同一份数据,更新时同步刷新。
Warning共享缓存的前提是 queryKey 完全相等(按前面讲的序列化规则)。key 差一点,比如
['todos']和['todos', undefined],就是两份独立缓存,不共享。
8.6 窗口聚焦重取
默认情况下,用户切到别的标签页再切回来,所有 stale 的查询会自动重新请求。这个行为叫 window focus refetch。
背后的逻辑:用户离开又回来,期间数据可能变了,主动刷新一下保证数据最新。
可以关掉:
// 全局关
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
// 单个查询关
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: false,
})
还可以设成 'always',即使数据是 fresh 的也重取:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: 'always',
})
Notev5 只监听
visibilitychange事件,不再监听focus事件。这意味着切标签页会触发,但在同一标签页里点别的输入框再点回来不会触发。更合理了。
React Native 里的焦点管理
React Native 没有 window,得手动把 AppState 接到 Query 的 focusManager:
import { AppState } from 'react-native'
import { focusManager } from '@tanstack/react-query'
function onAppStateChange(status) {
if (Platform.OS !== 'web') {
focusManager.setFocused(status === 'active')
}
}
useEffect(() => {
const subscription = AppState.addEventListener('change', onAppStateChange)
return () => subscription.remove()
}, [])
8.7 其他默认行为
除了 staleTime 和 gcTime,Query 还有几个默认行为值得知道:
网络重连重取
网络断了又连上,stale 的查询会自动重取。靠 refetchOnReconnect 控制,默认 true。
失败重试
请求失败默认重试 3 次,带指数退避。上一章讲过了。
结构共享(Structural Sharing)
Query 拿到新数据后,会和老数据做深度比较。如果数据实际没变(结构和值都一样),引用保持不变。这样配合 useMemo、useCallback 不会因为「数据没变但引用变了」触发无意义重渲染。
// 假设两次请求返回的数据内容完全一样
// data 的引用会保持不变,不会触发依赖 data 的 useMemo 重算
const { data } = useQuery({...})
Tip结构共享只对 JSON 兼容的值生效。如果你的数据里有 Map、Set、Date 这类,结构共享不工作,每次都会当成「变了」。大数据量遇到性能问题可以关掉:
structuralSharing: false。
8.8 staleTime 怎么设
这是最常被问的问题。没有标准答案,看场景:
| 场景 | 建议 staleTime |
|---|---|
| 实时性要求高(股价、消息) | 0(默认) |
| 普通列表数据 | 30 秒 - 2 分钟 |
| 不常变的数据(用户信息) | 5-10 分钟 |
| 几乎不变的数据(配置、字典) | Infinity 或 ‘static’ |
原则是:数据多久内你愿意接受旧值。能接受 1 分钟旧值,就设 1 分钟,省掉无谓的请求。
别把所有查询都设成 staleTime: 0 然后抱怨请求太多。合理设 staleTime 是 Query 性能调优的第一步。
8.9 小结
记住两个时间的本质:
- staleTime 管数据「新不新鲜」,影响是否触发自动重取
- gcTime 管缓存「留不留」,只在没有 active 观察者时生效
默认 staleTime 0、gcTime 5 分钟,是「激进但合理」的默认值。窗口聚焦、网络重连会自动重取 stale 数据。多个组件共享同 key 的缓存,请求自动去重。
下一篇讲查询状态,把 status、fetchStatus、isPending、isLoading 这些容易绕晕的概念理清楚。