2026/08/26

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