← [ BACK_TO_NOTES ]

Next.js Cache 与 TanStack Query 的关系

Next.js Cache 与 TanStack Query 都处理缓存身份、freshness 与 invalidation,但前者主要优化服务器/RSC 读取与渲染复用,后者管理浏览器中的 server-state projection 与交互期同步。

#synthesis #growing #frontend #react

[!info] related notes

Next.js Cache 与 TanStack Query 的关系

覆盖范围

Next.js Cache 与 TanStack Query 看起来都在做:

缓存
fresh / stale
失效
重新获取
mutation 后同步

所以很容易产生直觉:

“它们基本一样,只是 API 名字不同。”

这个类比在设计思想上有一部分是对的,但在运行层、共享范围、生命周期和 mutation 语义上差异很大。

最短结论:

Next.js Cache
→ server-side / RSC / rendering projection

TanStack Query
→ browser-side server-state projection

它们可以同时存在,而且经常应该同时存在。

共同的底层思想

两者确实共享几条非常重要的缓存原则。

1. Cache 不是 Truth

DB / Owning Service
      ↓
authoritative truth

Next.js Cache
      ↓
server projection

TanStack Query Cache
      ↓
client projection

冲突时,最终都必须向 authoritative source 收敛。

2. 缓存必须有身份

TanStack Query:

queryKey: ["goal", goalId]

Next.js:

cached function + arguments
+
cacheTag("goal:123")  // invalidation identity

两者都在解决:

我到底在缓存哪一份数据?

3. Freshness 与 Invalidation 是不同概念

两边都有:

按时间变旧
+
业务事件主动失效

这比“固定 TTL 以后全部重拉”更精确。

最大区别一:缓存运行在哪一层

Next.js Cache

主要发生在服务器 / Next.js rendering/data layer:

Browser
   ↓
Next.js Server
   ↓
[Next Cache]
   ↓
DB / API / Owning Service

它主要优化:

  • 重复服务器读取
  • RSC / Page / Component 计算
  • 服务端渲染结果复用
  • 多个请求共享的稳定数据读取

TanStack Query

主要发生在浏览器 QueryClient:

Browser
├── UI
└── [TanStack Query Cache]
        ↓
      API
        ↓
      Server

它主要解决:

  • 浏览器已经打开页面后的 server-state 生命周期
  • background refetch
  • window focus / reconnect 同步
  • mutation lifecycle
  • optimistic update
  • pagination / infinite query
  • query observer / loading / fetching 状态

所以:

Next Cache
= 服务端读取/渲染优化

TanStack Query
= 客户端远程状态同步

最大区别二:共享范围不同

TanStack Query 的 Query Cache 通常属于一个 QueryClient / 浏览器应用实例。

User A Browser
→ Query Cache A

User B Browser
→ Query Cache B

Next.js Server Cache 则运行在服务端,并可能跨请求复用缓存结果,因此跨用户隔离与 cache identity 更敏感。

这意味着:

私人用户数据
+
错误 server cache key
=
可能造成严重的数据隔离问题

所以 Next.js server cache 不能简单照搬“浏览器里 queryKey 写得差一点最多显示错状态”的风险模型。

queryKey 与 cacheTag 不是同一个东西

一个常见错误类比:

queryKey === cacheTag

不准确。

TanStack Query:

queryKey
→ Query Cache entry 的核心 identity

Next.js:

cached function + inputs
→ cache identity 的一部分

cacheTag
→ 给缓存增加可用于 invalidation 的业务标签

因此 cacheTag("products") 更接近:

“以后哪些缓存应该一起失效?”

而不是单纯等价于 Query Cache 地址。

staleTime 与 cacheLife 也不是一一对应

TanStack Query:

staleTime
→ 当前 Query projection 多久被视为 fresh

gcTime
→ inactive cache entry 多久后可被回收

Next.js Cache Components:

cacheLife.stale
→ Client Router 直接复用窗口

cacheLife.revalidate
→ Server Cache 后台刷新节奏

cacheLife.expire
→ Server Cache 必须重新生成的最大边界

所以:

cacheLife.stale ≠ staleTime 的同一层语义
cacheLife.expire ≠ gcTime

只能类比“都有 freshness/lifetime policy”,不能逐字段翻译。

revalidateTag 与 invalidateQueries

这两个是最接近的概念类比之一。

TanStack Query:

queryClient.invalidateQueries({
  queryKey: ["products"],
})

表达:

我不再相信客户端 projection 足够新,相关 Query 应重新同步。

Next.js:

revalidateTag("products", "max")

表达:

相关服务器缓存不应继续长期被视为 fresh,可以以 stale-while-revalidate 方式更新。

共同思想:

known-stale projection
→ revoke freshness trust
→ re-sync from authority

但它们操作的是两个不同缓存层。

updateTag 不是 TanStack Query 乐观更新

这点必须明确。

Next.js updateTag

Server mutation succeeds
        ↓
truth 已经改变
        ↓
updateTag()
        ↓
server cache 立即 expire
        ↓
下一次 read 获取 fresh result

它是:

confirmed write 后的 immediate server-cache invalidation。

TanStack Query Optimistic Update

Mutation 尚未确认
      ↓
客户端先预测成功
      ↓
UI / Query Cache 先改
      ↓
Server result
├─ success → reconcile
└─ failure → rollback

它是:

在 authority 确认之前做预测性 UI 更新。

所以:

updateTag
≠ optimistic update

如果 Next.js 应用需要乐观 UI,仍然可以使用 React useOptimistic、TanStack Query optimistic mutation 等客户端机制;updateTag 解决的是另一层问题。

updateTag 与 setQueryData 也不等价

TanStack Query:

queryClient.setQueryData(key, authoritativeResult)

可以直接把已知的新 projection 写进客户端 cache。

Next.js updateTag():

不直接告诉 cache 新值是什么
↓
只是让旧 server cache 立即失效
↓
下一次 read 再向 source 获取 fresh result

因此:

setQueryData
= client knows new projection

updateTag
= server cache old projection must not be reused

两层缓存如何一起工作

一个完整系统可能是:

PostgreSQL / Go Service
        ↓ truth

Next.js Server Cache
        ↓ server projection

RSC / HTML / API
        ↓

TanStack Query Cache
        ↓ client projection

Interactive UI

Mutation 后可能需要分别处理:

1. owning service commit
2. Next server cache invalidation
3. client Query cache reconciliation / invalidation

是否三步都需要,取决于实际数据路径。

一个 MemoFlow 示例

初次加载 /goals

Next Server
→ cached Goal list
→ RSC
→ Browser

浏览器进入复杂交互模式

TanStack Query
→ 管理 filter / refetch / optimistic mutation / background sync

用户创建 Goal

真正乐观方案可能是:

Client TanStack Query
→ 先显示 optimistic Goal

Server Action / API
→ Owning Service commit

Server
→ updateTag("goals")

Client
→ reconcile / invalidate Query

注意其中:

optimistic UI
和
server-cache invalidation

是两个独立动作。

多层缓存最大的风险

Go cache       5 min
Next cache    10 min
Query cache    3 min
CDN cache      5 min

如果没有清晰策略,会出现:

数据到底在哪一层旧了?谁负责 invalidation?

所以每一层至少要能回答:

Who owns truth?
What is cached?
What is the identity?
How stale may it be?
Who invalidates it?
How does it converge?

这也是 缓存策略与一致性 的通用原则。

选型总结

问题Next.js CacheTanStack Query
主要运行位置Server / RSC layerBrowser / QueryClient
核心目标复用服务器读取与渲染结果管理客户端 server-state projection
身份cached function/input + tagqueryKey
时间策略cacheLifestaleTime / gcTime
主动失效revalidateTag / updateTag / pathinvalidateQueries / setQueryData / removeQueries
乐观更新不是 updateTag 的职责一等使用场景
实时交互通常需配合 runtime streaming可和 SSE / WebSocket 协作

最短记忆

思想相似:
identity + freshness + invalidation + reconciliation

但不是同一层:
Next.js Cache = server projection
TanStack Query = client projection

最容易记错:
updateTag ≠ optimistic update
创建于 2026/9/15 更新于 2026/9/15
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本