2026/09/23

你的意見會幫助我們持續改善內容。
本文撰寫於 2026-09-23,內容以以下版本為基準:
| 技術 | 版本 |
|---|---|
| Next.js | 16.x |
| Router | App Router |
| Cache Components | Enabled |
| TanStack Query | v5 |
| React | 19 |
官方參考文件:
cacheLife 與 revalidation 的說明。stale、revalidate、expire 的正式定義。staleTime、refetch、inactive Query 與預設 5 分鐘 gcTime。cacheTime → gcTime 的改名原因與 v5 行為差異。如果未來 Next.js 升到 17 或 TanStack Query 升到 v6,建議重新核對 Router Cache、Cache Components、cacheLife 與 Query defaults。這幾個東西都是框架作者很喜歡「只是稍微調整一下」,然後整篇舊文章瞬間變歷史文獻的地方。在 Next.js App Router 專案加入 TanStack Query 後,很容易遇到一個問題:
「我現在看到的資料,到底是哪一層 Cache?」
以我的 Blog 專案為例,目前同時存在三種不同層級的快取:
usePosts():TanStack Query,負責文章列表資料。getPost() 裡的 "use cache":Next.js Server Cache,負責單篇文章資料。看起來全部都叫 Cache,但它們的快取內容、執行位置、生命週期與失效機制完全不同。
| Cache | 主要位置 | 快取內容 | 主要目的 |
|---|---|---|---|
| TanStack Query | Client | Query data / state | 管理 Client 端 Server State |
"use cache" | Server 為主 | function / component output | 避免 Server 重複取得或計算資料 |
| App Router Client Cache | Client | RSC Payload / route segments | 加速 App Router navigation |
假設文章列表使用:
function usePosts() {
return useQuery({
queryKey: ["posts"],
queryFn: getPosts,
});
}這裡使用的是 TanStack Query 的 Query Cache。
第一次成功取得資料後,Query 會包含:
["posts"] │ ▼┌─────────────────────┐│ TanStack Query Cache │├─────────────────────┤│ data ││ status ││ dataUpdatedAt ││ error ││ ... │└─────────────────────┘TanStack Query 官方將 QueryCache 定義為儲存 Query data、metadata 與 state 的機制。
換句話說,它不知道:
/post/post/foo/post/bar這些 Next.js Route 是什麼。
它只知道:
queryKey: ["posts"]因此 usePosts() 快取的是:
文章列表這份 Server State。
不是整個 /post 頁面,也不是 React Server Component Payload。
staleTime 不等於 Cache 存活時間這是 TanStack Query 最常被誤會的地方之一。
TanStack Query v5 預設:
staleTime: 0代表 Query 成功取得資料後,就會立即被視為 stale。
但:
stale 不代表資料被刪除。
例如:
useQuery({
queryKey: ["posts"],
queryFn: getPosts,
staleTime: 60 * 1000,
});表示:
60 秒內["posts"] = fresh在這段時間內,不會因一般的 mount、window focus 或 reconnect 等 stale-based trigger 自動重新取得資料。
真正控制 inactive Query 留在記憶體多久的則是:
gcTimeClient 預設:
gcTime: 5 * 60 * 1000也就是當 Query 已經沒有 observer 使用、進入 inactive 狀態後,預設 5 分鐘後 garbage collect。
因此:
staleTime↓多久以前算 fresh? gcTime↓沒有人使用後,多久從 Cache 移除?兩個完全不同。
TanStack Query v5 甚至把以前的:
cacheTime重新命名為:
gcTime官方給出的理由就是 cacheTime 很容易讓人誤以為:
「這筆資料只會 cache 這麼久。」
實際上它只在 Query inactive 之後才開始具有意義。
命名終於跟實際行為比較像了,人類為此少掉了一種可以吵架的東西。
getPost() 的 "use cache":Next.js Server Cache單篇文章則走另一條路。
例如:
export async function getPost(slug: string) {
"use cache";
const post = await fetchPost(slug);
return post;
}這裡的:
"use cache";跟 TanStack Query 完全沒有關係。
它是 Next.js 16 Cache Components 提供的 caching primitive。
使用前需要在:
// next.config.ts
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;啟用 Cache Components。
接著可以在:
使用:
"use cache";"use cache" 快取的是 function output例如:
getPost("nextjs-cache");第一次:
getPost("nextjs-cache") │ ▼ Cache Miss │ ▼ CMS / DB │ ▼ Post │ ▼ Cache Entry之後相同輸入再次執行:
getPost("nextjs-cache");如果 Cache Entry 仍有效,就可以重用 cached result。
Next.js 16 官方文件說明,"use cache" 的 Cache Key 會包含:
因此:
getPost("post-a");
getPost("post-b");因為 slug 不同,自然會形成不同 Cache Entry。
概念上:
getPost("post-a")│└─ Cache Entry A getPost("post-b")│└─ Cache Entry B這很適合 Blog Article Detail 這種資料。
因為文章通常:
讀取頻率高+修改頻率低+slug → 固定文章沒有必要每次 Server Render 都重新打一次 CMS。
"use cache" 不代表永遠都有一份全球共用 Cache這裡要特別注意部署環境。
Next.js 16 的一般 "use cache" 預設主要使用 in-memory cache。
官方文件目前說明:
| 部署方式 | Runtime caching |
|---|---|
| Self-hosted Node / Docker | Cache 可以跨 request 存在於同一 process |
| Serverless | 不一定跨 request 持續存在 |
所以不能簡化成:
「加了 "use cache",所有 Server Instance 永遠共用同一份 Cache。」不是。
如果有跨 instance、持久化 Cache 的需求,Next.js 16 另外提供:
"use cache: remote";以及自訂 cacheHandlers 的能力。
cacheLife() 控制 "use cache" 的 caching semantics可以搭配:
import { cacheLife } from "next/cache";
export async function getPost(slug: string) {
"use cache";
cacheLife({
stale: 300,
revalidate: 3600,
expire: 86400,
});
return fetchPost(slug);
}三個值的責任不同:
| 設定 | 主要意義 |
|---|---|
stale | Client 可以直接重用 cached content 的時間 |
revalidate | Server Cache 經過多久後可以背景重新產生 |
expire | Cached value 最久可以 stale 多久,超過後必須取得 fresh result |
特別注意:
stale不是 HTTP:
Cache-Control: max-ageNext.js 官方明確指出,它控制的是 Client-side Router Cache semantics。
因此 "use cache" 雖然是在 Server 宣告,但它的 Cache Lifetime 資訊並不是只活在 Server 世界。
Next.js 會將這些 caching semantics 傳遞給 Client Router。
最後是最容易跟 TanStack Query 搞混的一層。
假設使用者從:
/點擊:
/post/nextjs-cache透過:
<Link href="/post/nextjs-cache">進行 App Router navigation。
Next.js 不需要像傳統網站:
GET HTML→整頁 reload→重新建立整個畫面Next.js App Router 使用 React Server Components。
Server Components 會在 Server 被 render 成:
React Server Component Payload,RSC Payload
RSC Payload 裡會包含:
後續 client-side navigation 時,Next.js 可以 prefetch 並 cache 這些 RSC Payload,再使用它更新 React tree。
概念上:
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 管理。
這跟:
{
"title": "Next.js Cache",
"content": "..."
}這種 API data cache 是完全不同的東西。
這也是為什麼本文必須標版本。
Next.js 16 對 routing 與 navigation 系統做過重新設計,包括:
所以不要直接拿舊版文章裡:
「Router Cache 一律會 cache 整個 Page 五分鐘。」
套到 Next.js 16。
目前官方 prefetch 文件的預設行為,大致可以理解成:
| Route | Prefetch |
|---|---|
| Static Route | 可以 prefetch 完整 route |
| Dynamic Route | 預設不完整 prefetch |
Dynamic Route + loading.tsx | 可以 partial prefetch 到 loading boundary |
Next.js Client Cache 也會按照 route segment 重用已經取得的內容。
例如:
/dashboard/settings→/dashboard/analytics如果兩個 Route 共用:
/dashboard/layout.tsxClient 不需要重新下載相同 layout,而可以重用既有 segment。
現在就可以看到兩者的本質差異。
例如 /posts 頁面裡:
<PostList />而 PostList 使用:
usePosts();Browser 可能同時存在:
Browser│├── TanStack Query││ └── ["posts"]│ └── Post[]│└── Next.js Client Router └── Route Segments └── RSC PayloadTanStack Query 在回答:
["posts"] 這份 Server State 我手上有沒有?Next.js Router Cache 在回答:
這個 Route Segment 對應的 RSC Payload 我手上有沒有?
完全不同。
"use cache" 加進來完整架構大概會變成:
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 是正常的。
因為三層解決的是三個不同問題。
假設:
/posts→/post/nextjs-cache可能發生:
① 使用者點 <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 可能沒有:
/post/nextjs-cache RSC Payload但 Server 已經有:
getPost("nextjs-cache")的 cached result。
所以 Server 不一定需要重新向 CMS 取得文章。
我最後會把它記成:
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 |
"use cache"?這也是我目前專案的分工方式。
文章列表通常有:
filterpaginationsearchcategorysort這些本質上就是 Client 維護的 Server State。
例如:
["posts", { category, page }]TanStack Query 本身就已經提供:
因此:
Article List→ TanStack Query很合理。
文章詳細頁通常是:
slug↓post而且內容通常:
讀很多改很少SEO 重要Server Component 可以直接取得因此:
getPost(slug)放在 Server,再透過:
"use cache";控制 caching lifecycle。
Article Detail→ Next.js Server Cache也很合理。
看到這裡很容易產生一個危險想法:
既然 Cache 可以變快,那每一層全部 Cache 不就好了?不一定。
真正麻煩的是:
同一份資料由多套獨立 Cache 管理。
例如某篇文章更新後:
CMS↓Post v2 TanStack Query↓Post v2 "use cache"↓Post v1 Router Cache↓舊 RSC Payload這時就會出現經典前端場景:
API 明明是新的,為什麼畫面還是舊的?
接著開始:
F5清 Query Cache清 .nextrestart dev serverclear browser cache最後差點連自己的 SSD 都想格式化。
真正重要的不是:
Cache 有幾層?
而是:
每一層 Cache 到底負責什麼?誰負責 invalidation?
在這個 Blog 裡:
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 拆成三個完全不同的問題:
┌────────────────────────────┐│ 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。
所以下次看到:
為什麼這次沒有 Request?與其直接回答:
因為 Cache。更精確的問題其實是:
是哪一層根本沒有發出 Request?在 Next.js App Router 裡,Cache 早就不是單數了。