Next.js 中的快取機制

Next.js 透過快取渲染工作和資料請求,提升應用程式效能並降低成本。本頁深入探討 Next.js 的快取機制、可用的配置 API,以及它們之間的互動方式。

須知事項:本頁幫助您理解 Next.js 的底層運作,但這並非使用 Next.js 的必備知識。Next.js 的大部分快取策略由您的 API 使用方式決定,並提供最佳效能的預設值,無需或只需極少配置。若您想直接查看範例,請從這裡開始

概覽

以下是不同快取機制及其用途的高層次概覽:

機制快取內容位置用途持續時間
請求記憶化 (Request Memoization)函數回傳值伺服器在 React 元件樹中重複使用資料單次請求生命週期
資料快取 (Data Cache)資料伺服器跨使用者請求與部署儲存資料持久性(可重新驗證)
完整路由快取 (Full Route Cache)HTML 與 RSC 負載伺服器降低渲染成本並提升效能持久性(可重新驗證)
路由快取 (Router Cache)RSC 負載客戶端減少導航時的伺服器請求使用者會話或基於時間

預設情況下,Next.js 會盡可能快取以提升效能並降低成本。這意味著路由會靜態渲染且資料請求會被快取,除非您選擇退出。下圖顯示了預設的快取行為:當路由在建置時靜態渲染,以及當靜態路由首次被訪問時。

圖表顯示 Next.js 中四種機制的預設快取行為,包含在建置時和路由首次訪問時的 HIT、MISS 和 SET。

快取行為會根據路由是靜態或動態渲染、資料是否被快取,以及請求是首次訪問還是後續導航而變化。根據您的使用情境,您可以為個別路由和資料請求配置快取行為。

請求記憶化 (Request Memoization)

Next.js 擴展了 fetch API,自動記憶化具有相同 URL 和選項的請求。這意味著您可以在 React 元件樹的多個位置呼叫相同的 fetch 函數,而實際上只會執行一次。

去重複的 Fetch 請求

例如,如果您需要在路由中多處使用相同的資料(例如在 Layout、Page 和多個元件中),您不必在樹的頂層獲取資料並透過 props 傳遞。相反,您可以在需要資料的元件中直接獲取,而不必擔心因網路重複請求相同資料而影響效能。

async function getItem() {
  // `fetch` 函數會自動記憶化,結果會被快取
  const res = await fetch('https://.../item/1')
  return res.json()
}

// 此函數被呼叫兩次,但僅在第一次執行
const item = await getItem() // cache MISS

// 第二次呼叫可以位於路由的任何位置
const item = await getItem() // cache HIT
async function getItem() {
  // `fetch` 函數會自動記憶化,結果會被快取
  const res = await fetch('https://.../item/1')
  return res.json()
}

// 此函數被呼叫兩次,但僅在第一次執行
const item = await getItem() // cache MISS

// 第二次呼叫可以位於路由的任何位置
const item = await getItem() // cache HIT

請求記憶化的工作原理

圖表顯示 React 渲染期間 fetch 記憶化的工作方式。
  • 在渲染路由時,首次呼叫特定請求時,其結果不會在記憶體中,因此會是快取 MISS
  • 因此,函數會被執行,資料會從外部來源獲取,結果會儲存在記憶體中。
  • 同一渲染過程中對該請求的後續函數呼叫會是快取 HIT,資料會從記憶體中回傳而不執行函數。
  • 一旦路由渲染完成且渲染過程結束,記憶體會被「重置」,所有請求記憶化條目會被清除。

須知事項

  • 請求記憶化是 React 的功能,而非 Next.js 的功能。這裡提及是為了展示它與其他快取機制的互動。
  • 記憶化僅適用於 fetch 請求中的 GET 方法。
  • 記憶化僅適用於 React 元件樹,這意味著:
    • 適用於 generateMetadatagenerateStaticParams、Layouts、Pages 和其他 Server Components 中的 fetch 請求。
    • 不適用於 Route Handlers 中的 fetch 請求,因為它們不屬於 React 元件樹的一部分。
  • 對於不適合使用 fetch 的情況(例如某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端),您可以使用 React cache 函數 來記憶化函數。

持續時間

快取持續時間為伺服器請求的生命週期,直到 React 元件樹完成渲染。

重新驗證

由於記憶化不會跨伺服器請求共享且僅在渲染期間有效,因此無需重新驗證。

選擇退出

記憶化僅適用於 fetch 請求中的 GET 方法,其他方法如 POSTDELETE 不會被記憶化。此預設行為是 React 的優化,我們不建議選擇退出。

若要管理個別請求,您可以使用 AbortControllersignal 屬性。然而,這不會讓請求退出記憶化,而是中止進行中的請求。

app/example.js
const { signal } = new AbortController()
fetch(url, { signal })

資料快取 (Data Cache)

Next.js 內建了資料快取,可持久化資料獲取結果,跨伺服器請求部署。這是因為 Next.js 擴展了原生 fetch API,允許伺服器上的每個請求設定自己的持久化快取語義。

須知事項:在瀏覽器中,fetchcache 選項表示請求如何與瀏覽器的 HTTP 快取互動;在 Next.js 中,cache 選項表示伺服器端請求如何與伺服器的資料快取互動。

您可以使用 fetchcachenext.revalidate 選項來配置快取行為。

資料快取的工作原理

圖表顯示快取和非快取的 fetch 請求如何與資料快取互動。快取的請求儲存在資料快取中並被記憶化,非快取的請求從資料來源獲取,不儲存在資料快取中,但被記憶化。
  • 首次在渲染期間呼叫帶有 'force-cache' 選項的 fetch 請求時,Next.js 會檢查資料快取中是否有快取的回應。
  • 若找到快取的回應,會立即回傳並記憶化
  • 若未找到快取的回應,會向資料來源發出請求,結果儲存在資料快取中並記憶化。
  • 對於非快取資料(例如未定義 cache 選項或使用 { cache: 'no-store' }),結果總是從資料來源獲取並記憶化。
  • 無論資料是否被快取,請求總是會被記憶化,以避免在 React 渲染過程中對相同資料發出重複請求。

資料快取與請求記憶化的差異

雖然兩種快取機制都透過重複使用快取資料來提升效能,但資料快取跨請求與部署持久化,而記憶化僅在單次請求的生命週期內有效。

持續時間

資料快取跨請求與部署持久化,除非您重新驗證或選擇退出。

重新驗證

快取資料可透過兩種方式重新驗證:

  • 基於時間的重新驗證:在特定時間間隔後重新驗證資料並發出新請求。這適用於變更不頻繁且即時性不高的資料。
  • 按需重新驗證:基於事件(例如表單提交)重新驗證資料。按需重新驗證可使用基於標籤或路徑的方法一次性重新驗證一組資料。這適用於您希望盡快顯示最新資料的情況(例如當您的 headless CMS 內容更新時)。

基於時間的重新驗證

要按時間間隔重新驗證資料,您可以使用 fetchnext.revalidate 選項設定資源的快取生命週期(以秒為單位)。

// 最多每小時重新驗證一次
fetch('https://...', { next: { revalidate: 3600 } })

或者,您可以使用路由區段配置選項來配置區段中的所有 fetch 請求,或適用於無法使用 fetch 的情況。

基於時間的重新驗證工作原理

圖表顯示基於時間的重新驗證如何運作,在重新驗證期間後,首次請求會回傳過期資料,然後資料會被重新驗證。
  • 首次呼叫帶有 revalidate 的 fetch 請求時,資料會從外部資料來源獲取並儲存在資料快取中。
  • 在指定時間範圍內(例如 60 秒)的任何請求都會回傳快取的資料。
  • 時間範圍過後,下一次請求仍會回傳快取的(此時已過期)資料。
    • Next.js 會在背景觸發資料的重新驗證。
    • 一旦資料成功獲取,Next.js 會用新資料更新資料快取。
    • 若背景重新驗證失敗,先前的資料會保持不變。

這類似於 stale-while-revalidate 行為。

按需重新驗證

資料可以按路徑 (revalidatePath) 或快取標籤 (revalidateTag) 按需重新驗證。

按需重新驗證工作原理

圖表顯示按需重新驗證如何運作,在重新驗證請求後,資料快取會更新為新資料。
  • 首次呼叫 fetch 請求時,資料會從外部資料來源獲取並儲存在資料快取中。
  • 當觸發按需重新驗證時,相應的快取條目會從快取中清除。
    • 這與基於時間的重新驗證不同,後者會在獲取新資料前保留過期資料。
  • 下一次請求時會再次成為快取 MISS,資料會從外部資料來源獲取並儲存在資料快取中。

選擇退出

如果您不想快取 fetch 的回應,可以這樣做:

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

完整路由快取 (Full Route Cache)

相關術語

您可能會看到 Automatic Static OptimizationStatic Site GenerationStatic Rendering 這些術語交替使用,指在建置時渲染和快取應用程式路由的過程。

Next.js 會在建置時自動渲染和快取路由。這是一種優化,允許您提供快取的路由,而不是為每個請求在伺服器上渲染,從而實現更快的頁面載入。

要理解完整路由快取的工作原理,了解 React 如何處理渲染以及 Next.js 如何快取結果會有所幫助:

1. 伺服器上的 React 渲染

在伺服器上,Next.js 使用 React 的 API 來協調渲染。渲染工作被拆分為區塊:按個別路由區段和 Suspense 邊界。

每個區塊的渲染分為兩個步驟:

  1. React 將 Server Components 渲染為一種特殊的資料格式,專為串流優化,稱為 React Server Component Payload
  2. Next.js 使用 React Server Component Payload 和 Client Component 的 JavaScript 指令在伺服器上渲染 HTML

這意味著我們不必等待所有內容渲染完成後才快取工作或發送回覆。相反,我們可以在工作完成時串流回覆。

什麼是 React Server Component Payload?

React Server Component Payload 是渲染後的 React Server Components 樹的緊湊二進制表示。React 在客戶端使用它來更新瀏覽器的 DOM。React Server Component Payload 包含:

  • Server Components 的渲染結果
  • Client Components 應渲染位置的佔位符及其 JavaScript 檔案的參考
  • 從 Server Component 傳遞給 Client Component 的任何 props

要了解更多,請參閱 Server Components 文件。

2. Next.js 在伺服器上的快取(完整路由快取)

完整路由快取的預設行為,顯示靜態渲染路由的 React Server Component Payload 和 HTML 如何在伺服器上被快取。

Next.js 的預設行為是在伺服器上快取路由的渲染結果(React Server Component Payload 和 HTML)。這適用於在建置時靜態渲染的路由,或在重新驗證期間。

3. 客戶端的 React Hydration 與協調

在請求時,客戶端會:

  1. 使用 HTML 立即顯示 Client 和 Server Components 的非互動式初始預覽。
  2. 使用 React Server Components Payload 來協調 Client 和已渲染的 Server Component 樹,並更新 DOM。
  3. 使用 JavaScript 指令來 hydrate Client Components 並使應用程式可互動。

4. Next.js 在客戶端的快取(路由快取)

React Server Component Payload 儲存在客戶端的 路由快取 (Router Cache) 中——一個獨立的記憶體快取,按個別路由區段分割。此路由快取用於儲存先前訪問的路由和預取未來路由,以提升導航體驗。

5. 後續導航

在後續導航或預取期間,Next.js 會檢查 React Server Components Payload 是否儲存在路由快取中。若是,則會跳過向伺服器發送新請求。

若路由區段不在快取中,Next.js 會從伺服器獲取 React Server Components Payload,並在客戶端填充路由快取。

靜態與動態渲染 (Static and Dynamic Rendering)

路由是否在建置階段被快取,取決於它是靜態還是動態渲染。靜態路由預設會被快取,而動態路由則會在請求時才渲染,且不會被快取。

以下圖表展示了靜態與動態渲染路由的差異,以及快取與未快取資料的區別:

靜態與動態渲染如何影響完整路由快取 (Full Route Cache)。靜態路由在建置階段或資料重新驗證後會被快取,而動態路由永遠不會被快取

了解更多關於靜態與動態渲染的資訊。

持續時間 (Duration)

預設情況下,完整路由快取 (Full Route Cache) 是持久性的。這表示渲染輸出會在使用者請求之間被快取。

失效機制 (Invalidation)

有兩種方式可以讓完整路由快取失效:

  • 重新驗證資料 (Revalidating Data):重新驗證資料快取 (Data Cache) 會連帶使路由快取 (Router Cache) 失效,透過在伺服器端重新渲染元件並快取新的渲染輸出。
  • 重新部署 (Redeploying):與跨部署持續存在的資料快取不同,完整路由快取會在每次新部署時被清除。

選擇退出 (Opting out)

您可以選擇退出完整路由快取,換句話說,針對每個傳入請求動態渲染元件,方式如下:

  • 使用動態 API (Dynamic API):這會讓路由退出完整路由快取,並在請求時動態渲染。資料快取仍可使用。
  • 使用路由區段配置選項 dynamic = 'force-dynamic'revalidate = 0:這會跳過完整路由快取和資料快取。意味著元件會在每個傳入請求時重新渲染,並從伺服器重新取得資料。路由快取 (Router Cache) 仍會適用,因為它是客戶端快取。
  • 選擇退出資料快取 (Data Cache):如果路由有一個未快取的 fetch 請求,這會讓路由退出完整路由快取。該特定 fetch 請求的資料會在每個傳入請求時重新取得。其他未選擇退出快取的 fetch 請求仍會被快取在資料快取中。這允許快取與未快取資料的混合使用。

客戶端路由快取 (Client-side Router Cache)

Next.js 有一個記憶體中的客戶端路由快取,用於儲存路由區段的 RSC 負載 (payload),按佈局 (layouts)、載入狀態 (loading states) 和頁面 (pages) 分割。

當使用者在路由之間導航時,Next.js 會快取已造訪的路由區段,並預取 (prefetch) 使用者可能導航到的路由。這實現了即時的返回/前進導航、導航間無需整頁重新載入,並保留了 React 狀態和瀏覽器狀態。

透過路由快取:

  • 佈局 (Layouts) 在導航時會被快取並重複使用 (部分渲染 (partial rendering))。
  • 載入狀態 (Loading states) 在導航時會被快取並重複使用,以實現即時導航 (instant navigation)
  • 頁面 (Pages) 預設不會被快取,但在瀏覽器後退和前進導航時會被重複使用。您可以透過實驗性的 staleTimes 配置選項啟用頁面區段的快取。

須知事項 (Good to know):此快取專門適用於 Next.js 和伺服器元件 (Server Components),與瀏覽器的 bfcache 不同,儘管效果相似。

持續時間 (Duration)

快取儲存在瀏覽器的臨時記憶體中。兩個因素決定了路由快取的持續時間:

  • 工作階段 (Session):快取在導航間持續存在。然而,頁面重新整理時會被清除。
  • 自動失效週期 (Automatic Invalidation Period):佈局和載入狀態的快取會在特定時間後自動失效。持續時間取決於資源如何被預取 (prefetched),以及資源是否為靜態生成 (statically generated)
    • 預設預取 (Default Prefetching) (prefetch={null} 或未指定):動態頁面不會快取,靜態頁面快取 5 分鐘。
    • 完整預取 (Full Prefetching) (prefetch={true}router.prefetch):靜態和動態頁面均快取 5 分鐘。

雖然頁面重新整理會清除所有快取區段,但自動失效週期僅影響從預取時起的個別區段。

須知事項 (Good to know):實驗性的 staleTimes 配置選項可用於調整上述的自動失效時間。

失效機制 (Invalidation)

有兩種方式可以讓路由快取失效:

  • 伺服器動作 (Server Action) 中:
  • 呼叫 router.refresh 會使路由快取失效,並向伺服器發出新請求以取得目前路由。

選擇退出 (Opting out)

從 Next.js 15 開始,頁面區段預設為選擇退出。

須知事項 (Good to know):您也可以透過將 <Link> 元件的 prefetch 屬性設為 false選擇退出預取 (prefetching)

快取互動 (Cache Interactions)

在配置不同的快取機制時,了解它們如何互相影響非常重要:

資料快取與完整路由快取 (Data Cache and Full Route Cache)

  • 重新驗證或選擇退出資料快取使完整路由快取失效,因為渲染輸出依賴資料。
  • 使完整路由快取失效或選擇退出不會影響資料快取。您可以動態渲染一個同時包含快取和未快取資料的路由。這在頁面大部分使用快取資料,但少數元件依賴需在請求時取得的資料時非常有用。您可以動態渲染,而無需擔心重新取得所有資料對效能的影響。

資料快取與客戶端路由快取 (Data Cache and Client-side Router cache)

API 參考 (APIs)

下表概述了不同 Next.js API 如何影響快取:

API路由快取 (Router Cache)完整路由快取 (Full Route Cache)資料快取 (Data Cache)React 快取 (React Cache)
<Link prefetch>快取
router.prefetch快取
router.refresh重新驗證
fetch快取快取
fetch options.cache快取或選擇退出
fetch options.next.revalidate重新驗證重新驗證
fetch options.next.tags快取快取
revalidateTag重新驗證 (伺服器動作)重新驗證重新驗證
revalidatePath重新驗證 (伺服器動作)重新驗證重新驗證
const revalidate重新驗證或選擇退出重新驗證或選擇退出
const dynamic快取或選擇退出快取或選擇退出
cookies重新驗證 (伺服器動作)選擇退出
headers, searchParams選擇退出
generateStaticParams快取
React.cache快取
unstable_cache快取

預設情況下,<Link> 元件會自動從完整路由快取預取路由,並將 React 伺服器元件負載 (RSC payload) 加入路由快取。

若要停用預取,您可以將 prefetch 屬性設為 false。但這不會永久跳過快取,當使用者造訪路由時,路由區段仍會在客戶端被快取。

了解更多關於 <Link> 元件 的資訊。

router.prefetch

useRouter 鉤子的 prefetch 選項可用於手動預取路由。這會將 React 伺服器元件負載加入路由快取。

參閱 useRouter 鉤子 API 參考。

router.refresh

useRouter 鉤子的 refresh 選項可用於手動重新整理路由。這會完全清除路由快取,並向伺服器發出新請求以取得目前路由。refresh 不會影響資料或完整路由快取。

渲染結果會在客戶端進行協調 (reconcile),同時保留 React 狀態和瀏覽器狀態。

參閱 useRouter 鉤子 API 參考。

fetch

fetch 回傳的資料不會自動快取在資料快取中。

fetch 的預設快取行為(例如未指定 cache 選項時)等同於將 cache 選項設為 no-store

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

參閱 fetch API 參考 以取得更多選項。

fetch options.cache

您可以透過將 cache 選項設為 force-cache 來讓個別 fetch 選擇加入快取:

// 選擇加入快取
fetch(`https://...`, { cache: 'force-cache' })

參閱 fetch API 參考 以取得更多選項。

fetch options.next.revalidate

您可以使用 fetchnext.revalidate 選項來設定個別 fetch 請求的重新驗證週期(以秒為單位)。這會重新驗證資料快取,進而重新驗證完整路由快取。會取得新資料,並在伺服器端重新渲染元件。

// 最多 1 小時後重新驗證
fetch(`https://...`, { next: { revalidate: 3600 } })

參閱 fetch API 參考 以取得更多選項。

fetch options.next.tagsrevalidateTag

Next.js 有一個快取標籤系統,用於精細的資料快取和重新驗證。

  1. 使用 fetchunstable_cache 時,您可以選擇用一個或多個標籤標記快取項目。
  2. 然後,您可以呼叫 revalidateTag 來清除與該標籤關聯的快取項目。

例如,您可以在取得資料時設定標籤:

// 使用標籤快取資料
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

然後,使用標籤呼叫 revalidateTag 來清除快取項目:

// 重新驗證帶有特定標籤的項目
revalidateTag('a')

有兩個地方可以使用 revalidateTag,取決於您想達到的目標:

  1. 路由處理器 (Route Handlers) - 在第三方事件(例如 webhook)回應中重新驗證資料。這不會立即使路由快取失效,因為路由處理器不綁定到特定路由。
  2. 伺服器動作 (Server Actions) - 在使用者動作(例如表單提交)後重新驗證資料。這會使相關路由的路由快取失效。

revalidatePath

revalidatePath 允許您手動重新驗證資料在單一操作中重新渲染特定路徑下的路由區段。呼叫 revalidatePath 方法會重新驗證資料快取,進而使完整路由快取失效。

revalidatePath('/')

有兩個地方可以使用 revalidatePath,取決於您想達到的目標:

  1. 路由處理器 (Route Handlers) - 在第三方事件(例如 webhook)回應中重新驗證資料。
  2. 伺服器動作 (Server Actions) - 在使用者互動(例如表單提交、點擊按鈕)後重新驗證資料。

參閱 revalidatePath API 參考 以取得更多資訊。

revalidatePathrouter.refresh 的差異:

呼叫 router.refresh 會清除路由快取,並在伺服器端重新渲染路由區段,而不會使資料快取或完整路由快取失效。

差異在於 revalidatePath 會清除資料快取和完整路由快取,而 router.refresh() 不會改變資料快取和完整路由快取,因為它是客戶端 API。

動態 API (Dynamic APIs)

動態 API 如 cookiesheaders,以及頁面中的 searchParams 屬性,依賴於執行時傳入的請求資訊。使用它們會讓路由退出完整路由快取,換句話說,路由會被動態渲染。

cookies

在伺服器動作中使用 cookies.setcookies.delete 會使路由快取失效,以防止使用 cookies 的路由變得過時(例如反映驗證變更)。

參閱 cookies API 參考。

路由區段配置選項

路由區段配置選項 (Route Segment Config) 可用於覆寫路由區段的預設值,或當您無法使用 fetch API 時(例如使用資料庫客戶端或第三方函式庫)。

以下路由區段配置選項將會退出全路由快取 (Full Route Cache):

  • const dynamic = 'force-dynamic'

此配置選項將會讓所有 fetch 請求退出資料快取 (Data Cache)(即 no-store):

  • const fetchCache = 'default-no-store'

查看 fetchCache 以獲取更多進階選項。

查看 路由區段配置 文件以獲取更多選項。

generateStaticParams

對於 動態區段(例如 app/blog/[slug]/page.js),由 generateStaticParams 提供的路徑會在建置時快取在全路由快取中。在請求時,Next.js 也會在首次造訪時快取那些在建置時未知的路徑。

要在建置時靜態渲染所有路徑,請提供完整路徑列表給 generateStaticParams

app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

要在建置時靜態渲染部分路徑,並在執行時首次造訪時渲染其餘路徑,請返回部分路徑列表:

app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())

  // 在建置時渲染前 10 篇文章
  return posts.slice(0, 10).map((post) => ({
    slug: post.slug,
  }))
}

要在首次造訪時靜態渲染所有路徑,請返回空陣列(建置時不會渲染任何路徑)或使用 export const dynamic = 'force-static'

app/blog/[slug]/page.js
export async function generateStaticParams() {
  return []
}

須知事項: 您必須從 generateStaticParams 返回一個陣列,即使是空的。否則,路由將會被動態渲染。

app/changelog/[slug]/page.js
export const dynamic = 'force-static'

要在請求時停用快取,請在路由區段中添加 export const dynamicParams = false 選項。當使用此配置選項時,只有由 generateStaticParams 提供的路徑會被服務,其他路由將會返回 404 或匹配(在 萬用路由 的情況下)。

React cache 函式

React cache 函式允許您記憶化函式的返回值,讓您可以多次呼叫同一個函式,但只執行一次。

由於 fetch 請求會自動記憶化,您不需要將其包裹在 React cache 中。然而,當 fetch API 不適用時,您可以使用 cache 手動記憶化資料請求。例如,某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端。

import { cache } from 'react'
import db from '@/lib/db'

export const getItem = cache(async (id: string) => {
  const item = await db.item.findUnique({ id })
  return item
})
import { cache } from 'react'
import db from '@/lib/db'

export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ id })
  return item
})