Next.js 中的快取機制

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

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

概覽

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

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

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

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

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

請求記憶化

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

去重複的 Fetch 請求

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

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 和其他伺服器元件中的 fetch 請求。
    • 它不適用於路由處理器中的 fetch 請求,因為它們不屬於 React 元件樹的一部分。
  • 對於不適合使用 fetch 的情況(如某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端),您可以使用 React cache 函數 來記憶化函數。

持續時間

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

重新驗證

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

選擇退出

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

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

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

資料快取

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' })

完整路由快取

相關術語

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

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

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

1. 伺服器上的 React 渲染

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

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

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

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

什麼是 React Server Component Payload?

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

  • 伺服器元件的渲染結果
  • 客戶端元件應渲染位置的佔位符及其 JavaScript 檔案的參考
  • 從伺服器元件傳遞到客戶端元件的任何 props

要了解更多,請參閱伺服器元件文件。

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

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

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

3. 客戶端的 React 水合與協調

在請求時,客戶端會:

  1. 使用 HTML 立即顯示客戶端和伺服器元件的快速非互動式初始預覽。
  2. 使用 React Server Components Payload 來協調客戶端和已渲染的伺服器元件樹,並更新 DOM。
  3. 使用 JavaScript 指令來水合客戶端元件,使應用程式具有互動性。

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

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

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 負載 (RSC payload),這些區段按佈局 (layouts)、載入狀態 (loading states) 和頁面 (pages) 分割。

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

使用路由快取時:

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

須知:此快取專門適用於 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 分鐘。

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

須知:實驗性的 staleTimes 設定選項可用於調整上述的自動失效時間。

失效機制 (Invalidation)

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

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

選擇退出 (Opting out)

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

須知:您也可以透過將 <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 不會影響資料快取或完整路由快取。

渲染結果會在客戶端進行協調,同時保留 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 不適用時(例如某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端),您可以使用 cache 手動記憶化資料請求。

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
})