跳至內容

Next.js 的快取機制

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

注意事項:本頁可協助您了解 Next.js 的底層運作方式,但對於有效使用 Next.js 並必要知識。大多數 Next.js 的快取策略取決於您的 API 使用方式,並已設定預設值,以便在零或最小設定下獲得最佳效能。如果您想直接查看範例,請由此開始

概覽

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

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

預設情況下,Next.js 會盡可能快取更多內容以提升效能並降低成本。這表示除非您選擇停用,否則路由會進行靜態渲染,而資料請求會被快取。下圖顯示了預設的快取行為:在建置時靜態渲染路由,以及首次訪問靜態路由時。

Diagram showing the default caching behavior in Next.js for the four mechanisms, with HIT, MISS and SET at build time and when a route is first visited.

快取行為會根據路由是靜態渲染還是動態渲染、資料是否已快取,以及請求是初始訪問的一部分還是後續導覽而有所不同。您可以根據您的使用案例,為個別路由和資料請求設定快取行為。

請求記憶體化

React 擴充了 fetch API,以自動記憶體化具有相同 URL 和選項的請求。這表示您可以在 React 元件樹中的多個位置呼叫具有相同資料的 fetch 函式,但只會執行一次。

Deduplicated Fetch Requests

例如,如果您需要在整個路由中使用相同的資料(例如,在佈局、頁面和多個元件中),您不需要在樹的頂部擷取資料,並在元件之間傳遞 props。您可以直接在需要資料的元件中擷取資料,而不必擔心在網路上針對相同資料發出多個請求的效能影響。

app/example.tsx
async function getItem() {
  // The `fetch` function is automatically memoized and the result
  // is cached
  const res = await fetch('https://.../item/1')
  return res.json()
}
 
// This function is called twice, but only executed the first time
const item = await getItem() // cache MISS
 
// The second call could be anywhere in your route
const item = await getItem() // cache HIT

請求記憶化 (Request Memoization) 的運作方式

Diagram showing how fetch memoization works during React rendering.
  • 在渲染路由的過程中,第一次呼叫特定請求時,其結果不會儲存在記憶體中,這將會是快取 `MISS` (未命中)。
  • 因此,函式將會被執行,資料將會從外部來源取得,然後結果將會儲存在記憶體中。
  • 在同一次渲染過程中,後續對該請求的函式呼叫將會是快取 `HIT` (命中),並且資料將會從記憶體中返回,而不會再次執行函式。
  • 一旦路由渲染完成,且渲染過程結束,記憶體就會被「重置」,所有請求記憶化項目都會被清除。

注意事項:

  • 請求記憶化是 React 的功能,而不是 Next.js 的功能。這裡包含它是為了說明它與其他快取機制的互動方式。
  • 記憶化僅適用於 `fetch` 請求中的 `GET` 方法。
  • 記憶化僅適用於 React 元件樹,這表示:
    • 它適用於 `generateMetadata`、`generateStaticParams`、佈局 (Layouts)、頁面 (Pages) 和其他伺服器元件 (Server Components) 中的 `fetch` 請求。
    • 它不適用於路由處理程式 (Route Handlers) 中的 `fetch` 請求,因為它們不是 React 元件樹的一部分。
  • 對於 `fetch` 不適合的情況(例如某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端),您可以使用 React 的 `cache` 函式 來記憶化函式。

持續時間

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

重新驗證

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

停用

記憶化僅適用於 `fetch` 請求中的 `GET` 方法,其他方法(例如 `POST` 和 `DELETE`)則不會被記憶化。此預設行為是 React 的一種優化,我們不建議停用它。

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

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

資料快取

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

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

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

資料快取的運作方式

Diagram showing how cached and uncached fetch requests interact with the Data Cache. Cached requests are stored in the Data Cache, and memoized, uncached requests are fetched from the data source, not stored in the Data Cache, and memoized.
  • 在渲染期間第一次呼叫帶有 'force-cache' 選項的 fetch 請求時,Next.js 會檢查資料快取中是否有快取的回應。
  • 如果找到快取的回應,它會立即返回並進行記憶體化
  • 如果沒有找到快取的回應,則會向資料來源發出請求,將結果儲存在資料快取中,並進行記憶體化。
  • 對於未快取的資料(例如,未定義 cache 選項或使用 { cache: 'no-store' }),結果會始終從資料來源擷取,並進行記憶體化。
  • 無論資料是否已快取,請求都會始終進行記憶體化,以避免在 React 渲染過程中對相同資料發出重複的請求。

資料快取和請求記憶體化之間的差異

雖然兩種快取機制都透過重複使用快取的資料來協助提升效能,但資料快取會在連入請求和部署之間持久化,而記憶體化只會持續一個請求的生命週期。

持續時間

除非您重新驗證或選擇退出,否則資料快取會在連入請求和部署之間持久化。

重新驗證

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

  • 基於時間的重新驗證:在經過一段時間後,當新的請求發出時,重新驗證資料。這適用於不常變更且新鮮度不那麼重要的資料。
  • 按需重新驗證:根據事件(例如表單提交)重新驗證資料。按需重新驗證可以使用基於標籤或基於路徑的方法一次重新驗證多組資料。當您想要確保盡快顯示最新資料時,這非常有用(例如,當您的無頭 CMS 中的內容更新時)。

基於時間的重新驗證

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

// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })

或者,您可以使用路由區段設定選項來設定區段中所有 fetch 請求,或是在您無法使用 fetch 的情況下進行設定。

基於時間的重新驗證如何運作

Diagram showing how time-based revalidation works, after the revalidation period, stale data is returned for the first request, then data is revalidated.
  • 第一次呼叫帶有 revalidate 的 fetch 請求時,資料將會從外部資料來源擷取並儲存在資料快取中。
  • 在指定的時間範圍內(例如 60 秒)呼叫的任何請求都將返回快取的資料。
  • 時間範圍過後,下一個請求仍然會返回快取的(現在已過期的)資料。
    • Next.js 會在背景觸發資料的重新驗證。
    • 一旦資料成功擷取,Next.js 就會使用新的資料更新資料快取。
    • 如果背景重新驗證失敗,先前的資料將保持不變。

這類似於 stale-while-revalidate(過期時重新驗證) 的行為。

隨需重新驗證

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

隨需重新驗證如何運作

Diagram showing how on-demand revalidation works, the Data Cache is updated with fresh data after a revalidation request.
  • 第一次呼叫 fetch 請求時,資料將會從外部資料來源擷取並儲存在資料快取中。
  • 當觸發隨需重新驗證時,相應的快取項目將會從快取中清除。
    • 這與基於時間的重新驗證不同,基於時間的重新驗證會將過期資料保留在快取中,直到擷取到新的資料為止。
  • 下次發出請求時,它將再次成為快取 MISS,並且資料將會從外部資料來源擷取並儲存在資料快取中。

停用

如果您*不想*快取 fetch 的回應,您可以執行以下操作

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

完整路由快取

相關術語:

您可能會看到自動靜態優化靜態網站生成靜態渲染等術語互換使用,它們指的是在建置時渲染和快取應用程式路由的過程。

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

要了解完整路由快取的工作原理,最好先了解 React 如何處理渲染,以及 Next.js 如何快取結果。

1. 伺服器端的 React 渲染

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

每個區塊都分兩個步驟渲染:

  1. React 將伺服器組件渲染成一種特殊的資料格式,稱為React 伺服器組件有效負載 (React Server Component Payload),該格式已針對串流進行優化。
  2. Next.js 使用 React 伺服器組件有效負載和客戶端組件 JavaScript 指令在伺服器上渲染 HTML

這表示我們不必等到所有內容都渲染完畢才能快取工作或傳送回應。相反地,我們可以在工作完成時串流傳送回應。

什麼是 React 伺服器組件有效負載?

React 伺服器組件有效負載是已渲染的 React 伺服器組件樹的精簡二進位表示法。React 在客戶端上使用它來更新瀏覽器的 DOM。React 伺服器組件有效負載包含:

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

欲了解更多資訊,請參閱伺服器組件文件。

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

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

3. 用戶端上的 React 激活與協調

在請求時,於用戶端上

  1. HTML 會被用來立即顯示用戶端和伺服器組件的快速、非互動式初始預覽。
  2. React 伺服器組件有效負載用於協調用戶端和已渲染的伺服器組件樹,並更新 DOM。
  3. JavaScript 指令用於激活用戶端組件,並使應用程式具有互動性。

4. 用戶端上的 Next.js 快取(路由器快取)

React 伺服器組件有效負載儲存在用戶端路由器快取中 — 一個獨立的記憶體快取,按個別路由區段分割。此路由器快取用於儲存先前訪問過的路由和預取未來路由,以改善導航體驗。

5. 後續導航

在後續導航或預取期間,Next.js 會檢查 React 伺服器組件有效負載是否儲存在路由器快取中。如果是,它將略過向伺服器發送新請求的步驟。

如果路由區段不在快取中,Next.js 將從伺服器擷取 React 伺服器組件有效負載,並填入用戶端上的路由器快取。

靜態和動態渲染

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

此圖顯示了靜態渲染和動態渲染路由之間的差異,以及快取和未快取的資料。

How static and dynamic rendering affects the Full Route Cache. Static routes are cached at build time or after data revalidation, whereas dynamic routes are never cached

深入瞭解靜態和動態渲染

持續時間

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

失效

您可以透過兩種方式使完整路由快取失效

  • 重新驗證資料:重新驗證資料快取將會使路由器快取失效,方法是重新渲染伺服器上的元件並快取新的渲染輸出。
  • 重新部署:與跨部署持續存在的資料快取不同,完整路由快取會在新部署時清除。

停用

您可以選擇停用完整路由快取,換句話說,為每個傳入請求動態渲染元件,方法如下:

  • 使用動態 API:這將使路由退出完整路由快取,並在請求時動態渲染。資料快取仍然可以使用。
  • 使用 dynamic = 'force-dynamic'revalidate = 0 路由區段設定選項:這將略過完整路由快取和資料快取。這表示元件將會在每個傳入伺服器的請求時渲染和擷取資料。路由器快取仍會套用,因為它是用戶端快取。
  • 停用資料快取:如果路由的 fetch 請求未快取,則會使該路由退出完整路由快取。特定 fetch 請求的資料將會針對每個傳入請求進行擷取。其他未停用快取的 fetch 請求仍會快取在資料快取中。這允許快取和未快取資料的混合使用。

用戶端路由器快取

Next.js 有一個記憶體內用戶端路由器快取,用於儲存路由區段的 RSC 負載,按佈局、載入狀態和頁面劃分。

當使用者在路由之間導覽時,Next.js 會快取已訪問的路由區段,並預先擷取使用者可能導覽到的路由。這會導致即時的後退/前進導覽,在導覽之間沒有整頁重新載入,並保留 React 狀態和瀏覽器狀態。

使用路由器快取

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

注意事項:此快取專門應用於 Next.js 和伺服器元件,與瀏覽器的 bfcache 不同,儘管它們有類似的結果。

持續時間

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

  • 工作階段 (Session):快取在導航過程中會持續存在。但是,它會在重新整理頁面時被清除。
  • 自動失效期限:版面配置和載入狀態的快取會在特定時間後自動失效。持續時間取決於資源是如何被 預取 的,以及資源是否被 靜態產生
    • 預設預取 (prefetch={null} 或未指定):動態頁面不快取,靜態頁面快取 5 分鐘。
    • 完整預取 (prefetch={true}router.prefetch):靜態和動態頁面皆快取 5 分鐘。

雖然重新整理頁面會清除**所有**已快取的區段,但自動失效期限只會影響從預取時間開始計算的個別區段。

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

失效

您可以透過兩種方式讓路由器快取失效:

  • 在**伺服器動作**中:
  • 呼叫 router.refresh 將使路由器快取失效,並向伺服器發出新的請求以取得目前的路由。

停用

從 Next.js 15 開始,頁面區段預設為停用快取。

注意事項:您也可以透過將 <Link> 元件的 prefetch 屬性設為 false 來停用 預取

快取互動

在設定不同的快取機制時,了解它們之間如何互動非常重要。

資料快取和完整路由快取

  • 重新驗證或停用資料快取**將**使完整路由快取失效,因為渲染輸出取決於資料。
  • 將完整路由快取失效或選擇停用並不會影響資料快取。您可以動態渲染同時具有已快取和未快取資料的路由。當您頁面上的大部分內容使用已快取的資料,但有一些組件依賴需要在請求時提取的資料時,這會很有用。您可以進行動態渲染,而不必擔心重新提取所有資料的效能影響。

資料快取和客戶端路由器快取

  • 要立即失效資料快取和路由器快取,您可以在revalidatePathrevalidateTag中使用伺服器動作
  • 路由處理器中重新驗證資料快取不會立即失效路由器快取,因為路由處理器並未綁定到特定路由。這表示路由器快取將繼續提供先前的有效負載,直到強制重新整理或自動失效期限到期為止。

API
API路由器快取完整路由快取 (Full Route Cache)資料快取 (Data Cache)React 快取
<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 伺服器組件有效負載添加到路由器快取中。

要停用預提取,您可以將 prefetch 屬性設為 false。但这不会永久跳过缓存,当用户访问该路由时,路由片段仍会在客户端缓存。

深入瞭解 <Link> 組件

router.prefetch

useRouter hook 的 prefetch 選項可用於手動預提取路由。這會將 React 伺服器組件有效負載添加到路由器快取中。

請參閱 useRouter hook API 參考。

router.refresh

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

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

請參閱 useRouter hook API 參考。

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

有關更多選項,請參閱 fetch API 參考

fetch options.cache
// Opt into caching
fetch(`https://...`, { cache: 'force-cache' })

有關更多選項,請參閱 fetch API 參考

fetch options.next.revalidate
// Revalidate at most after 1 hour
fetch(`https://...`, { next: { revalidate: 3600 } })

有關更多選項,請參閱 fetch API 參考

fetch options.next.tagsrevalidateTag

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

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

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

// Cache data with a tag
fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })

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

// Revalidate entries with a specific tag
revalidateTag('a')

根據您嘗試達成的目標,您可以使用 revalidateTag 的兩個地方

  1. 路由處理器 - 根據第三方事件(例如 Webhook)重新驗證數據。這不會立即失效路由器快取,因為路由處理器並未綁定到特定路由。
  2. 伺服器動作 - 在使用者操作(例如表單提交)後重新驗證數據。這將使相關聯路由的路由器快取失效。

revalidatePath

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

revalidatePath('/')

根據您的目標,有兩個地方可以使用 revalidatePath

  1. 路由處理器 - 根據第三方事件(例如 Webhook)重新驗證數據。
  2. 伺服器動作 - 在使用者互動(例如表單提交、點擊按鈕)後重新驗證數據。

更多資訊請參閱 revalidatePath API 參考

revalidatePath vs. router.refresh

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

不同之處在於 revalidatePath 會清除數據快取和完整路由快取,而 router.refresh() 不會更改數據快取和完整路由快取,因為它是客戶端 API。

動態 API

cookiesheaders 這樣的動態 API,以及頁面中的 searchParams 屬性,取決於運行時傳入的請求資訊。使用它們將會使路由跳出完整路由快取,換句話說,路由將會被動態渲染。

cookies

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

請參閱 cookies API 參考。

區段配置選項

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

以下路由區段設定選項將停用完整路由快取

此設定選項將使所有提取操作停用資料快取(即 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())
 
  // Render the first 10 posts at build time
  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 中。但是,您可以使用 cache 手動記憶資料請求,以供 fetch API 不適用時使用。例如,某些資料庫客戶端、CMS 客戶端或 GraphQL 客戶端。

utils/get-item.ts
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
})