Next.js + TanStack Query 的 Cache 到底在快取什麼?一次搞懂三層快取

2026/09/23

深入解析 Next.js 16 與 TanStack Query v5 的三層快取機制,搞懂 Query Cache、use cache 與 App Router Client Cache 的差異、生命週期與適用情境。
Monochrome Next.js concepts banner with a stylized N

版本與參考資料

內容以以下版本為基準:

技術版本
Next.js16.x
RouterApp Router
Cache ComponentsEnabled
TanStack Queryv5
React19

官方參考文件:

Next.js

  • use cache Directive
    Next.js 官方對 Cache Components、Cache Key、runtime caching、cacheLife 與 revalidation 的說明。
  • cacheLife API
    stale、revalidate、expire 的正式定義。
  • Prefetching
    Next.js 16 Client Cache、Route Segment、Static / Dynamic Route prefetch 行為。
  • Linking and Navigating
    App Router 的 Server Rendering、Prefetch、Streaming 與 Client-side Transition。
  • Server and Client Components
    RSC Payload 的內容與 subsequent navigation 行為。
  • Upgrading to Next.js 16
    Next.js 16 routing/navigation overhaul、Layout Deduplication 與 Incremental Prefetching。

TanStack Query

  • QueryCache
    TanStack Query Query Cache 的正式定義。
  • Important Defaults
    staleTime、refetch、inactive Query 與預設 5 分鐘 gcTime。
  • Caching Examples
    Query 從 active → inactive → garbage collection 的生命週期。
  • Migrating to TanStack Query v5
    cacheTime → gcTime 的改名原因與 v5 行為差異。
如果未來 Next.js 升到 17 或 TanStack Query 升到 v6,建議重新核對 Router Cache、Cache Components、cacheLife 與 Query defaults。這幾個東西都是框架作者很喜歡「只是稍微調整一下」,然後整篇舊文章瞬間變歷史文獻的地方。

背景

在 Next.js App Router 專案加入 TanStack Query 後,很容易遇到一個問題:

「我現在看到的資料,到底是哪一層 Cache?」

以我的 Blog 專案為例,目前同時存在三種不同層級的快取:

  1. usePosts():TanStack Query,負責文章列表資料。
  2. getPost() 裡的 "use cache":Next.js Server Cache,負責單篇文章資料。
  3. App Router Client Cache:Next.js 在 Client 端保存 navigation 所需的 RSC Payload。

看起來全部都叫 Cache,但它們的快取內容、執行位置、生命週期與失效機制完全不同。

Cache主要位置快取內容主要目的
TanStack QueryClientQuery data / state管理 Client 端 Server State
"use cache"Server 為主function / component output避免 Server 重複取得或計算資料
App Router Client CacheClientRSC Payload / route segments加速 App Router navigation

1. TanStack Query:快取「資料」

假設文章列表使用:

ts
function usePosts() {
  return useQuery({
    queryKey: ["posts"],
    queryFn: getPosts,
  });
}

這裡使用的是 TanStack Query 的 Query Cache。

第一次成功取得資料後,Query 會包含:

txt
["posts"]     │     ▼┌─────────────────────┐│ TanStack Query Cache │├─────────────────────┤│ data                 ││ status               ││ dataUpdatedAt        ││ error                ││ ...                  │└─────────────────────┘

TanStack Query 官方將 QueryCache 定義為儲存 Query data、metadata 與 state 的機制。

換句話說,它不知道:

txt
/post/post/foo/post/bar

這些 Next.js Route 是什麼。

它只知道:

ts
queryKey: ["posts"]

因此 usePosts() 快取的是:

文章列表這份 Server State。

不是整個 /post 頁面,也不是 React Server Component Payload。

staleTime 不等於 Cache 存活時間

這是 TanStack Query 最常被誤會的地方之一。

TanStack Query v5 預設:

ts
staleTime: 0

代表 Query 成功取得資料後,就會立即被視為 stale。

但:

stale 不代表資料被刪除。

例如:

ts
useQuery({
  queryKey: ["posts"],
  queryFn: getPosts,
  staleTime: 60 * 1000,
});

表示:

txt
60 秒內["posts"] = fresh

在這段時間內,不會因一般的 mount、window focus 或 reconnect 等 stale-based trigger 自動重新取得資料。

真正控制 inactive Query 留在記憶體多久的則是:

ts
gcTime

Client 預設:

ts
gcTime: 5 * 60 * 1000

也就是當 Query 已經沒有 observer 使用、進入 inactive 狀態後,預設 5 分鐘後 garbage collect。

因此:

txt
staleTime↓多久以前算 fresh? gcTime↓沒有人使用後,多久從 Cache 移除?

兩個完全不同。

TanStack Query v5 甚至把以前的:

ts
cacheTime

重新命名為:

ts
gcTime

官方給出的理由就是 cacheTime 很容易讓人誤以為:

「這筆資料只會 cache 這麼久。」

實際上它只在 Query inactive 之後才開始具有意義。

命名終於跟實際行為比較像了,人類為此少掉了一種可以吵架的東西。

2. getPost() 的 "use cache":Next.js Server Cache

單篇文章則走另一條路。

例如:

ts
export async function getPost(slug: string) {
  "use cache";

  const post = await fetchPost(slug);

  return post;
}

這裡的:

ts
"use cache";

跟 TanStack Query 完全沒有關係。

它是 Next.js 16 Cache Components 提供的 caching primitive。

使用前需要在:

ts
// next.config.ts

const nextConfig = {
  cacheComponents: true,
};

export default nextConfig;

啟用 Cache Components。

接著可以在:

  • async function
  • React Component
  • module / file

使用:

ts
"use cache";

"use cache" 快取的是 function output

例如:

ts
getPost("nextjs-cache");

第一次:

txt
getPost("nextjs-cache")        │        ▼      Cache Miss        │        ▼      CMS / DB        │        ▼       Post        │        ▼     Cache Entry

之後相同輸入再次執行:

ts
getPost("nextjs-cache");

如果 Cache Entry 仍有效,就可以重用 cached result。

Next.js 16 官方文件說明,"use cache" 的 Cache Key 會包含:

  • Build ID
  • Function ID
  • Serializable arguments
  • 被 closure 捕捉並影響 function 的值
  • Development 下還包含 HMR refresh hash

因此:

ts
getPost("post-a");
getPost("post-b");

因為 slug 不同,自然會形成不同 Cache Entry。

概念上:

txt
getPost("post-a")│└─ Cache Entry A getPost("post-b")│└─ Cache Entry B

這很適合 Blog Article Detail 這種資料。

因為文章通常:

txt
讀取頻率高+修改頻率低+slug → 固定文章

沒有必要每次 Server Render 都重新打一次 CMS。

注意:"use cache" 不代表永遠都有一份全球共用 Cache

這裡要特別注意部署環境。

Next.js 16 的一般 "use cache" 預設主要使用 in-memory cache。

官方文件目前說明:

部署方式Runtime caching
Self-hosted Node / DockerCache 可以跨 request 存在於同一 process
Serverless不一定跨 request 持續存在

所以不能簡化成:

「加了 "use cache",所有 Server Instance 永遠共用同一份 Cache。」

不是。

如果有跨 instance、持久化 Cache 的需求,Next.js 16 另外提供:

ts
"use cache: remote";

以及自訂 cacheHandlers 的能力。

cacheLife() 控制 "use cache" 的 caching semantics

可以搭配:

ts
import { cacheLife } from "next/cache";

export async function getPost(slug: string) {
  "use cache";

  cacheLife({
    stale: 300,
    revalidate: 3600,
    expire: 86400,
  });

  return fetchPost(slug);
}

三個值的責任不同:

設定主要意義
staleClient 可以直接重用 cached content 的時間
revalidateServer Cache 經過多久後可以背景重新產生
expireCached value 最久可以 stale 多久,超過後必須取得 fresh result

特別注意:

ts
stale

不是 HTTP:

http
Cache-Control: max-age

Next.js 官方明確指出,它控制的是 Client-side Router Cache semantics。

因此 "use cache" 雖然是在 Server 宣告,但它的 Cache Lifetime 資訊並不是只活在 Server 世界。

Next.js 會將這些 caching semantics 傳遞給 Client Router。

3. App Router Client Cache:快取 RSC Payload

最後是最容易跟 TanStack Query 搞混的一層。

假設使用者從:

txt
/

點擊:

txt
/post/nextjs-cache

透過:

tsx
<Link href="/post/nextjs-cache">

進行 App Router navigation。

Next.js 不需要像傳統網站:

txt
GET HTML→整頁 reload→重新建立整個畫面

Next.js App Router 使用 React Server Components。

Server Components 會在 Server 被 render 成:

React Server Component Payload,RSC Payload

RSC Payload 裡會包含:

  • Server Component render result
  • Client Component placeholder
  • Client Component JavaScript reference
  • Server → Client Component props

後續 client-side navigation 時,Next.js 可以 prefetch 並 cache 這些 RSC Payload,再使用它更新 React tree。

Client Cache 存的是 Route Segment Payload

概念上:

txt
Browser ┌──────────────────────────────┐│ Next.js Client Router Cache  │├──────────────────────────────┤│ layout segment               ││ loading segment              ││ page / prefetched segment    ││ RSC Payload                  │└──────────────────────────────┘

Next.js 16 的 prefetch 文件描述 Client Cache:

Prefetched RSC Payload 會儲存在 memory,並依 Route Segment 管理。

這跟:

json
{
  "title": "Next.js Cache",
  "content": "..."
}

這種 API data cache 是完全不同的東西。

Next.js 16 的 Router Cache 要特別注意版本差異

這也是為什麼本文必須標版本。

Next.js 16 對 routing 與 navigation 系統做過重新設計,包括:

  • Layout deduplication
  • Incremental prefetching
  • 更細粒度的 route segment reuse

所以不要直接拿舊版文章裡:

「Router Cache 一律會 cache 整個 Page 五分鐘。」

套到 Next.js 16。

目前官方 prefetch 文件的預設行為,大致可以理解成:

RoutePrefetch
Static Route可以 prefetch 完整 route
Dynamic Route預設不完整 prefetch
Dynamic Route + loading.tsx可以 partial prefetch 到 loading boundary

Next.js Client Cache 也會按照 route segment 重用已經取得的內容。

例如:

txt
/dashboard/settings→/dashboard/analytics

如果兩個 Route 共用:

txt
/dashboard/layout.tsx

Client 不需要重新下載相同 layout,而可以重用既有 segment。

TanStack Query Cache vs Router Cache

現在就可以看到兩者的本質差異。

例如 /posts 頁面裡:

tsx
<PostList />

而 PostList 使用:

ts
usePosts();

Browser 可能同時存在:

txt
Browser│├── TanStack Query││   └── ["posts"]│       └── Post[]│└── Next.js Client Router    └── Route Segments        └── RSC Payload

TanStack Query 在回答:

["posts"] 這份 Server State 我手上有沒有?

Next.js Router Cache 在回答:

這個 Route Segment 對應的 RSC Payload 我手上有沒有?

完全不同。

再把 "use cache" 加進來

完整架構大概會變成:

txt
                     Browser      ┌────────────────────────────┐      │                            │      │ TanStack Query             │      │                            │      │ ["posts"] → Post[]         │      │                            │      ├────────────────────────────┤      │                            │      │ Next.js Client Router      │      │                            │      │ route → RSC Payload        │      │                            │      └──────────────┬─────────────┘                     │              client navigation                     │                     ▼                Next.js Server                     │      ┌──────────────▼─────────────┐      │                            │      │ "use cache"                │      │                            │      │ getPost(slug) → Post       │      │                            │      └──────────────┬─────────────┘                     │                  Cache Miss                     │                     ▼                  CMS / API

所以同一個 Blog 同時存在三種 Cache 是正常的。

因為三層解決的是三個不同問題。

一次 Navigation 到底可能發生什麼?

假設:

txt
/posts→/post/nextjs-cache

可能發生:

txt
① 使用者點 <Link>         ↓ ② Next.js Client Router    有沒有可重用 / prefetched 的 RSC Payload?         ↓ 沒有 ③ 向 Server 請求需要的 RSC Payload         ↓ ④ Server Render         ↓ ⑤ getPost("nextjs-cache")         ↓ ⑥ "use cache"    有沒有對應 Cache Entry?         ↓ 有 ⑦ 不重新查 CMS         ↓ ⑧ Server 完成 RSC Payload         ↓ ⑨ Client 更新 React Tree

這裡最重要的是:

Client Router Cache Miss,不代表 Server Data Cache Miss。

Client 可能沒有:

txt
/post/nextjs-cache RSC Payload

但 Server 已經有:

txt
getPost("nextjs-cache")

的 cached result。

所以 Server 不一定需要重新向 CMS 取得文章。

三層 Cache 的責任邊界

我最後會把它記成:

txt
TanStack Query=資料還要不要拿? "use cache"=Server 還要不要重新 fetch / compute? App Router Client Cache=Navigation 還要不要向 Server 取得這段 RSC Payload?

整理成表格:

問題負責的 Cache
posts API 要不要重新取得?TanStack Query
getPost(slug) 要不要重新查 CMS?"use cache"
Navigation 是否已有可重用 RSC Payload?Next.js Client Router Cache

為什麼文章列表用 TanStack Query,文章詳細頁用 "use cache"?

這也是我目前專案的分工方式。

Article List

文章列表通常有:

txt
filterpaginationsearchcategorysort

這些本質上就是 Client 維護的 Server State。

例如:

ts
["posts", { category, page }]

TanStack Query 本身就已經提供:

  • Query Key
  • stale state
  • background refetch
  • loading / error state
  • invalidation
  • garbage collection

因此:

txt
Article List→ TanStack Query

很合理。

Article Detail

文章詳細頁通常是:

txt
slug↓post

而且內容通常:

txt
讀很多改很少SEO 重要Server Component 可以直接取得

因此:

ts
getPost(slug)

放在 Server,再透過:

ts
"use cache";

控制 caching lifecycle。

txt
Article Detail→ Next.js Server Cache

也很合理。

Cache 不是越多越好

看到這裡很容易產生一個危險想法:

txt
既然 Cache 可以變快,那每一層全部 Cache 不就好了?

不一定。

真正麻煩的是:

同一份資料由多套獨立 Cache 管理。

例如某篇文章更新後:

txt
CMS↓Post v2 TanStack Query↓Post v2 "use cache"↓Post v1 Router Cache↓舊 RSC Payload

這時就會出現經典前端場景:

API 明明是新的,為什麼畫面還是舊的?

接著開始:

txt
F5清 Query Cache清 .nextrestart dev serverclear browser cache

最後差點連自己的 SSD 都想格式化。

真正重要的不是:

Cache 有幾層?

而是:

每一層 Cache 到底負責什麼?誰負責 invalidation?

在這個 Blog 裡:

txt
Posts Collection→ TanStack Query Post Detail Server Data→ Next.js "use cache" Navigation / RSC Payload→ Next.js Client Router

責任就清楚很多。

最後整理

Next.js App Router 搭配 TanStack Query,可以把 Cache 拆成三個完全不同的問題:

txt
┌────────────────────────────┐│ TanStack Query             ││                            ││ Query Data / Server State  │└────────────────────────────┘ ┌────────────────────────────┐│ Next.js "use cache"        ││                            ││ Server function /          ││ component output           │└────────────────────────────┘ ┌────────────────────────────┐│ App Router Client Cache    ││                            ││ RSC Payload /              ││ Route Segments             │└────────────────────────────┘

一句話總結:

TanStack Query 快取資料,"use cache" 快取 Server function / component 的輸出,而 App Router Client Cache 快取 Client Navigation 所需要的 RSC Payload。

所以下次看到:

txt
為什麼這次沒有 Request?

與其直接回答:

txt
因為 Cache。

更精確的問題其實是:

txt
是哪一層根本沒有發出 Request?

在 Next.js App Router 裡,Cache 早就不是單數了。


這篇文章有幫助嗎?