2026/08/26

本文內容以以下版本為基準:
{
"next": "16.3.3",
"react": "19.2.8",
"@tanstack/react-query": "5.102.3"
}主要討論 Next.js App Router 的 Cache Components、use cache、React cache(),以及它們與 TanStack Query 在資料快取上的職責差異。
Next.js 的 caching API 仍持續演進,如果使用不同版本,實際行為與 API 請以對應版本的官方文件為準。
在 Next.js App Router 裡,cache 這個詞出現得有點太頻繁。
你可能會同時看到:
use cacheReact.cache()cacheLife()cacheTag()名字都很像,但其實它們解決的是完全不同的問題。
如果只想先建立正確觀念,可以先記住這三句:
React.cache():避免同一次 Server Request 重複查資料。use cache:讓不同 Request 之間共用 Server Cache。 TanStack Query:管理 Client 端的 Server State。
假設我們有一個取得文章的 function:
async function getPosts() {
return db.post.findMany()
}使用者第一次進入頁面:
Request A→ getPosts()→ 查詢資料庫接著另一個 Request 進來:
Request B→ getPosts()→ 再查一次資料庫如果使用 use cache:
async function getPosts() {
'use cache'
return db.post.findMany()
}就可能變成:
Request A→ 查 DB→ 建立 Cache Request B→ 使用 Cache Request C→ 使用 Cache也就是說,前一個 Request 結束後,Cache 還可以繼續被後續 Request 使用。
這就是所謂的「跨 Request Cache」。
React.cache() 又是什麼?React.cache() 不一樣。
例如:
import { cache } from 'react'
export const getUser = cache(async () => {
return db.user.findFirst()
})假設同一個 Server Render 裡:
Layout → getUser()Page → getUser()Header → getUser()雖然呼叫了三次,但實際上只會執行一次查詢。
Request A Layout ─┐Page ─┼→ getUser() → DB 一次Header ─┘但是下一個 Request:
Request B→ getUser()→ 又會重新查 DB因此 React.cache() 比較像:
Request scoped memoization
它的目的是避免「同一次 Request 裡重複做相同工作」,不是建立長期的資料 Cache。
use cache 適合什麼資料?因為 use cache 可以跨 Request 共用結果,所以很適合:
| 資料 | 適合嗎 |
|---|---|
| Blog 文章 | ✅ |
| CMS 公開內容 | ✅ |
| 商品列表 | ✅ |
| 公開股票基本資料 | ✅ |
| SEO 頁面資料 | ✅ |
| 國家、產業分類 | ✅ |
| Session | ❌ |
| 使用者權限 | ❌ |
| 個人購物車 | 通常不建議 |
| 個人持股 | 通常不建議 |
簡單來說:
公開、多人共用,而且不需要每秒保持最新的資料,非常適合 `use cache`。
例如 Blog:
async function getPosts() {
'use cache'
return db.post.findMany()
}所有使用者看到的文章資料本來就一樣,因此共用 Cache 很合理。
假設你要取得登入使用者資料:
async function getCurrentUser() {
const session = await getSession()
return db.user.findUnique({
where: {
id: session.userId,
},
})
}這類資料通常不適合隨便做跨 Request Cache。
因為它涉及:
如果 Cache Key 或失效策略設計錯誤,後果可能不是「資料舊了一點」,而是:
User A→ 個人資料被 Cache User B→ 意外取得 User A 資料這種 bug 就不是 UX 問題,而是資安問題。
因此對 Session 或 Authorization,我通常更偏向:
import { cache } from 'react'
export const verifySession = cache(async () => {
// 驗證登入狀態
})這樣可以避免:
LayoutPageDAL在同一次 Request 裡重複驗證 Session,但又不會把結果跨 Request 保存。
use cache 要怎麼控制多久更新?可以搭配 cacheLife():
import { cacheLife } from 'next/cache'
async function getPosts() {
'use cache'
cacheLife('hours')
return db.post.findMany()
}概念很簡單:
use cache→ 要不要 Cache cacheLife→ Cache 可以活多久例如:
| 資料 | 可能的策略 |
|---|---|
| Blog | hours / days |
| 商品資料 | minutes / hours |
| 靜態分類 | days / weeks |
不用一開始就鑽進所有 stale、revalidate、expire 細節。
先知道:
Cache 不是永久的,cacheLife() 負責控制生命週期。就已經足夠應付大部分場景。
例如文章後台新增了一篇文章。
如果原本:
async function getPosts() {
'use cache'
return db.post.findMany()
}那 Cache 裡可能還是舊文章。
這時可以加入 Tag:
import { cacheTag } from 'next/cache'
async function getPosts() {
'use cache'
cacheTag('posts')
return db.post.findMany()
}文章更新後:
updateTag('posts')概念就是:
getPosts() ↓Cache: posts 新增文章 ↓updateTag('posts') ↓舊 Cache 失效這樣比單純設定「每 60 秒重新抓一次」更合理。
因為資料根本沒有改的時候,就不需要一直重新查詢。
有關,但 PPR 本身不是 Cache API。
可以先這樣理解:
| 概念 | 負責什麼 |
|---|---|
use cache | 決定哪些資料或 UI 可以被重用 |
| PPR | 決定頁面哪些部分可以先送出、哪些部分等 Request 再 Render |
Suspense | 切出 Dynamic Content 的邊界 |
例如一個商品頁:
<ProductPage>
<Header />
<ProductInfo />
<Suspense fallback={<Skeleton />}>
<UserCart />
</Suspense>
</ProductPage>其中:
ProductPage├─ Header│ └─ 可預先 Render│├─ ProductInfo│ └─ use cache│└─ UserCart └─ Request-time Dynamic Content如果沒有 PPR,頁面裡只要出現需要 Request 才知道結果的資料,例如 Cookie、Session 或使用者購物車,很容易讓整個頁面都必須等 Server Render 完才回傳。
PPR 的目的就是避免這件事。
Request ↓先回傳 Prerendered / Cached Shell ↓Header + ProductInfo 先顯示 ↓UserCart 再透過 Suspense Stream 回來所以可以把兩者的關係理解成:
use cache ↓哪些內容可以預先準備、重用 ↓PPR ↓先回傳這些內容組成的 Shell ↓Dynamic Content Request 時再 Render但不要把兩個概念混在一起:
Cache= 資料或 UI 能不能重用 PPR= 頁面的 Rendering Strategy也就是說,use cache 解決的是 「這份結果要不要重複利用」;
PPR 解決的是 「這個頁面能不能先把已經準備好的部分送出去,不必等所有 Dynamic Content」。
這也是 Next.js App Router 很重要的一個觀念轉變。
以前比較容易把頁面想成:
Static PagevsDynamic Page現在則更適合想成:
Page├─ Cached / Prerendered Content│└─ Dynamic Content └─ Suspense因此,一個頁面不需要因為某個 Component 讀取 Session 或 Cookie,就整頁全部變成需要等待的 Dynamic Rendering。
簡單記一句:
`use cache` 決定哪些內容可以重用;PPR 則讓這些已經準備好的內容先送給使用者,再透過 Suspense 補上 Request-time Dynamic Content。
React.cache() 和 use cache 都不能直接取代 TanStack Query。
因為 TanStack Query 處理的是另一件事:
Client 端 Server State 管理。
例如:
useQuery({
queryKey: ['stocks'],
queryFn: getStocks,
staleTime: 60_000,
})它除了 Cache,還包含:
因此可以把三者理解成:
Server│├─ React.cache()│ └─ 同一個 Request 去重│├─ use cache│ └─ 跨 Request Server Cache│└─ Client └─ TanStack Query ├─ Query Cache ├─ Refetch ├─ Mutation └─ Server State可以直接使用這個判斷方式。
例如:
Blog商品資料公開股票資料CMSSEO Content使用:
'use cache'必要時搭配:
cacheLife()
cacheTag()如果只是避免同一次 Request 重複查詢:
React.cache()例如:
const verifySession = cache(async () => {
// ...
})如果需要:
使用:
TanStack Query其實不用把 Next.js Cache 想得太神祕。
記住這張表就夠了:
| 工具 | 解決什麼問題 |
|---|---|
React.cache() | 同一次 Request 避免重複執行 |
use cache | 不同 Request 共用 Server Cache |
cacheLife() | 控制 Server Cache 壽命 |
cacheTag() | 標記 Cache |
updateTag() | 資料更新後讓 Cache 失效 |
| TanStack Query | Client Server State 管理 |
最重要的一個原則是:
Public shared data 適合跨 Request Cache;Session、權限與敏感的 User-specific data 則應該保守處理。
看到 API 名字都有 cache 不代表它們是在做同一件事。否則照這個命名邏輯,冰箱、冷凍庫跟便當盒大概也能算同一種架構。