Next.js 筆記 - Hydration & Serialization 以及四種快取機制探討
在實際使用 Next.js 的 app router 開發時,除了直覺的檔案結構外,其實「Serialization 與 Hydration 的原理」以及「四層快取機制」的運作方式也變得非常重要。
這篇筆記將系統性整理 Hydration/Serialization 流程與 Next.js 主要快取策略,並結合自身開發經驗,方便日後查閱與排解疑難。
Hydration 與 Dehydration 是什麼?
React Server Components(RSC)會先在伺服器生成 HTML;Hydration 則於瀏覽器端注入 JavaScript,使原本靜態的標記具備互動性。相對地,Dehydration 會將可序列化的狀態(通常為 JSON)封裝並傳送至另一端,於下次請求時再進行 hydrate 以恢復狀態。可以類比為旅途中先將行李壓縮打包(dehydrate),抵達目的地後再展開(hydrate),確保 SSR 與 CSR 無縫銜接。
Hydration 與 Dehydration 的對照
| 階段 | 執行位置 | 主要動作 |
|---|---|---|
| Dehydration | 伺服器 → HTML | 將記憶體中的狀態(如 RSC 樹、React Query Cache)序列化為 JSON,可嵌入 HTML/RSC Payload。 |
| Hydration | 瀏覽器 → JS | React 讀取序列化結果,還原物件 並 掛載事件,使靜態 HTML 變為可互動。 |
兩者是一組「寫出/讀回」過程:正確序列化是成功 Hydration 的前提。
為什麼需要序列化?
- 跨邊界:僅當資料必須在 Server ↔ Client 間傳遞(如 Server Component 將 props 傳給 Client Component,或 Server Action 回傳到瀏覽器)時,React 會將值寫入 RSC Payload。
- 目的:確保瀏覽器端能在 Hydration 階段還原(hydrate)出對應的 JavaScript 物件,並掛上事件監聽器。
NOTE: 簡單來說, 序列化 就是定義「Server 可以傳哪些值到 Client」。
允許序列化的型別
| 類別 | 可序列化的型別 |
|---|---|
| 原始型別 | string · number · bigint · boolean · null · undefined · 全域 Symbol (Symbol.for) |
| 可迭代集合 | Array · String · Map · Set · 所有 TypedArray · ArrayBuffer |
| 特殊值 | Date · Promise · 純物件 (Plain Object) |
| React 相關 | Server / Client Component 的 JSX element · 標記 'use server' 的 Server Function |
不可序列化的常見案例
- 非 Server Function 的一般 函式
- Class 及其實例
- 非全域 Symbol(如
Symbol('foo')) nullprototype 物件
傳遞這些值會在 Build 或 Hydration 階段拋錯。
四種快取機制
Next.js 15 提供「層層遞進」的快取模型,從最短暫到最持久依序為:Request Memoization → Data Cache → Full Route Cache → Router Cache。以下用故事化敘事,把抽象名詞變成場景。
1. Request Memoization:同一次請求,只做一次工
情境:<Layout>、<Sidebar> 和 <Page> 同時呼叫 GET /api/users。
在同一個伺服器請求生命週期裡,Next.js 會先檢查 fetch 的 URL、HTTP 方法(僅限 GET)、init 參數是否完全相同。如果符合,就把第一次的 Promise 回傳值暫存於記憶體,再將之分享給其他需要的元件。請求結束,記憶體即被釋放,任何後續請求都要重新來過。
限制
- 僅
GET方法具備自動去重功能;POST /users仍會分別執行。 - Memoization 不跨請求、不跨使用者,更不會寫入磁碟。
NOTE: 這種快取機制讓開發者不必再糾結於「所有 fetch 一定要寫在最上層(如 layout 或 page)」的傳統做法。我們可以在任何需要資料的元件(甚至是巢狀元件)中直接呼叫 fetch,只要請求參數相同,Next.js 會自動共用同一份資料,避免重複請求。
2. Data Cache:後端的「公用冰箱」
情境:/products 靜態列表一小時才更新一次。
Data Cache 就是把 fetch 拿到的資料,直接存到寫入可持久化的伺服端快取上(像 Vercel 的 Global Data Cache 或我們自己主機預設的記憶體+檔案系統)。這樣一來,不管是哪個使用者、發出多少次請求,都會拿到同一份快取資料。這份資料會一直存在,直到我們設定的時間到了,或我們手動把它清掉。
- 如何使用:在
fetch()所在檔案(RSC 或 Server Action)設定next: { cache: 'force-cache' }即可。 - 有效期限:可透過
next: { revalidate: 60 }指定秒數;到期後第一次請求仍使用舊資料,但背景會 revalidate。 - 主動失效:
revalidatePath('/products'):以路徑為基準。revalidateTag('product'):以標籤分類。
NOTE: 以 Next.js 15 而言,fetch 在未指定 cache 參數時,相當於將 cache 設為 no-store。
NOTE: Data Cache 被失效時,若該路由同時有 Full Route Cache,Next.js 會連同整頁重新渲染並覆寫舊快取。
3. Full Route Cache:整盤料理,煮好再端上桌
情境:部落格文章於建置階段就渲染為靜態 HTML。
Full Route Cache 把整個路由的最終 render 結果(HTML + RSC Payload)寫入硬碟。使用者請求相同 URL(含 query string)時,伺服器直接吐出現成結果,完全不跑 React Tree——真正的「0ms TTFB」。
- 生成時機:
npm run build時由 SSG 產生。- 或在
dynamic = 'force-static'、revalidate = 60等模式下,第一次請求時由 ISR 背景產生。
- 如何跳脫:
- 使用 Dynamic API(
cookies,headers,searchParams)。 - 在
route.ts內寫POST、PUT等非GEThandler。 dynamic = 'force-dynamic'或revalidate = 0明示關閉。
- 使用 Dynamic API(
- 持久性:部署新版本會整包清空,但 Data Cache 不受影響。
4. Router Cache:瀏覽器端的時間膠囊
情境:使用者由 /posts 點擊到 /posts/1,再按瀏覽器返回。
Router Cache 儲存在瀏覽器記憶體,主要目標是加速導航與 Prefetch。當我們滑到頁面中某連結時,Next.js 可能已先向伺服器要 RSC Payload 並存下;之後點擊不需再等待請求就能渲染,體感近似 SPA。
- 失效條件:
- 執行
router.refresh()。 - 在 Server Action 呼叫
revalidatePath或revalidateTag。 - 在 Server Action 呼叫
cookies.set()或cookies.delete()(避免登入狀態改變卻仍顯舊畫面)。
- 執行
NOTE: 只對 Client 端生效;直接打 URL 或硬刷新仍會重新造訪伺服器。
快取層之間的連動規則
| 被失效 | 連動影響 |
|---|---|
| Data Cache | 同路由的 Full Route Cache 也會被重新渲染 |
| Full Route Cache | Data Cache 仍保留;新渲染會優先讀舊 Data Cache |
| Router Cache | 與伺服器快取(Data / Full Route)無直接關聯 |
| Request Memoization | 生命週期最短,不影響任何其他層 |
實務 Tips
- 資料變動快? fetch 資料時,cache 機制以
no-store為主。 - 需要 SEO? 靜態生產或 ISR,留給 Full Route Cache。
- 跨 page 共用 JSON? 用 Data Cache + revalidateTag。
- 單次請求多處重複 fetch? 交給 Request Memoization,避免多餘的 API Request。
- 登入、購物車一變就要即時? 操作 cookies 後
router.refresh(),確保 Router Cache 清乾淨。