跳到內容
App Router入門快取元件

快取元件

快取元件是 Next.js 中一種新的渲染和快取方法,它透過**部分預渲染 (PPR)** 提供對快取內容和時間的細粒度控制,同時確保出色的使用者體驗。

快取元件

在開發動態應用程式時,您必須平衡兩種主要方法

  • **完全靜態頁面**載入速度快,但無法顯示個性化或即時資料
  • **完全動態頁面**可以顯示最新資料,但需要為每個請求渲染所有內容,導致初始載入速度變慢

啟用快取元件後,Next.js 預設將所有路由視為**動態路由**。每個請求都使用最新可用資料進行渲染。然而,大多數頁面由靜態和動態部分組成,並非所有動態資料都需要在每個請求中從源解析。

快取元件允許您將資料甚至部分 UI 標記為可快取,這些內容將與頁面的靜態部分一起包含在預渲染過程中。

**在快取元件之前**,Next.js 嘗試自動靜態最佳化**整個**頁面,這在新增動態程式碼時可能會導致意外行為。

快取元件實現了**部分預渲染 (PPR)** 和 `use cache`,為您提供了兩全其美的解決方案

Partially re-rendered Product Page showing static nav and product information, and dynamic cart and recommended products

當用戶訪問路由時

  • 伺服器傳送一個**靜態 shell**,其中包含快取內容,確保快速初始載入
  • 包裹在 `Suspense` 邊界中的動態部分在 shell 中顯示回退 UI
  • 只有動態部分會渲染以替換其回退,並在準備好時並行流式傳輸
  • 您可以透過使用 `use cache` 快取資料,將原本動態的資料包含在初始 shell 中

**🎥 觀看:** PPR 的原理及工作方式 → YouTube(10 分鐘)

工作原理

快取元件為您提供了三個關鍵工具來控制渲染

1. 執行時資料的 Suspense

某些資料僅在實際使用者發出請求的執行時可用。`cookies`、`headers` 和 `searchParams` 等 API 訪問請求特定資訊。將使用這些 API 的元件包裝在 `Suspense` 邊界中,以便頁面的其餘部分可以作為靜態 shell 進行預渲染。

執行時 API 包括

2. 動態資料的 Suspense

動態資料,如 `fetch` 呼叫或資料庫查詢 (`db.query(...)`),可能會在請求之間發生變化,但與使用者無關。`connection` API 是元動態的——它表示等待使用者導航,即使沒有實際資料可返回。將使用這些內容的元件包裝在 `Suspense` 邊界中以啟用流式傳輸。

動態資料模式包括

3. 使用 `use cache` 快取資料

將 `use cache` 新增到任何伺服器元件,使其被快取幷包含在預渲染的 shell 中。您不能在快取元件中使用執行時 API。您還可以將實用函式標記為 `use cache` 並從伺服器元件中呼叫它們。

export async function getProducts() {
  'use cache'
  const data = await db.query('SELECT * FROM products')
  return data
}

使用 Suspense 邊界

React Suspense 邊界允許您定義在包裝動態或執行時資料時使用的回退 UI。

邊界外部的內容(包括回退 UI)作為靜態 shell 進行預渲染,而邊界內部的內容在準備好後流式傳輸。

以下是如何將 `Suspense` 與快取元件一起使用

app/page.tsx
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<Skeleton />}>
        <DynamicContent />
      </Suspense>
    </>
  )
}
 
async function DynamicContent() {
  const res = await fetch('http://api.cms.com/posts')
  const { posts } = await res.json()
  return <div>{/* ... */}</div>
}

在構建時,Next.js 預渲染靜態內容和 `fallback` UI,而動態內容則推遲到使用者請求路由時。

**須知**:將元件包裝在 `Suspense` 中並不會使其變為動態;您的 API 使用才是。`Suspense` 充當封裝動態內容並啟用流式傳輸的邊界。

缺少 Suspense 邊界

快取元件強制動態程式碼必須包裝在 `Suspense` 邊界中。如果您忘記了,您將看到 `<Suspense>` 外部訪問了未快取的資料 錯誤

在 `<Suspense>` 外部訪問了未快取的資料

這會延遲整個頁面的渲染,導致使用者體驗緩慢。Next.js 使用此錯誤來確保您的應用程式在每次導航時都能即時載入。

要解決此問題,您可以

**將元件包裝在 `<Suspense>`** 邊界中。這允許 Next.js 在內容準備好後立即將其流式傳輸給使用者,而不會阻塞應用程式的其餘部分。

或者

**將非同步 await 移動到快取元件("use cache")中**。這允許 Next.js 將元件作為 HTML 文件的一部分靜態預渲染,因此使用者可以即時看到它。

請注意,請求特定資訊(如 params、cookies 和 headers)在靜態預渲染期間不可用,因此必須將其包裝在 `<Suspense>` 中。

此錯誤有助於防止出現使用者無法立即獲得靜態 shell 的情況,取而代之的是遇到一個阻塞的執行時渲染,卻沒有任何內容可顯示。要解決此問題,請新增 `Suspense` 邊界或使用 `use cache` 來快取工作。

流式傳輸的工作原理

流式傳輸將路由分成多個塊,並在準備好時逐步將它們流式傳輸到客戶端。這允許使用者立即看到頁面的部分內容,而無需等到整個內容渲染完成。

Diagram showing partially rendered page on the client, with loading UI for chunks that are being streamed.

透過部分預渲染,初始 UI 可以立即傳送到瀏覽器,同時動態部分進行渲染。這減少了 UI 時間,並可能根據預渲染的 UI 量減少總請求時間。

Diagram showing parallelization of route segments during streaming, showing data fetching,rendering, and hydration of individual chunks.

為了減少網路開銷,包括靜態 HTML 和流式傳輸動態部分的完整響應會透過**單個 HTTP 請求**傳送。這避免了額外的往返,並改善了初始載入和整體效能。

使用 `use cache`

雖然 `Suspense` 邊界管理動態內容,但 `use cache` 指令可用於快取不經常更改的資料或計算。

基本用法

新增 `use cache` 以快取頁面、元件或非同步函式,並使用 `cacheLife` 定義生命週期

app/page.tsx
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  // fetch or compute
  return <div>...</div>
}

注意事項

使用 `use cache` 時,請記住這些限制

引數必須可序列化

與伺服器操作類似,快取函式的引數必須可序列化。這意味著您可以傳遞原始型別、普通物件和陣列,但不能傳遞類例項、函式或其他複雜型別。

在不自省的情況下接受不可序列化的值

您可以接受不可序列化的值作為引數,只要您不自省它們。但是,您可以返回它們。這允許像快取元件接受伺服器或客戶端元件作為子元件的模式

app/cached-wrapper.tsx
import { ReactNode } from 'react'
 
export async function CachedWrapper({ children }: { children: ReactNode }) {
  'use cache'
  // Don't introspect children, just pass it through
  return (
    <div className="wrapper">
      <header>Cached Header</header>
      {children}
    </div>
  )
}

避免傳遞動態輸入

您不得將動態或執行時資料傳遞給 `use cache` 函式,除非您避免自省它們。將 `cookies()`、`headers()` 或其他執行時 API 的值作為引數傳遞會導致錯誤,因為在預渲染時無法確定快取鍵。

標籤和重新驗證

使用 `cacheTag` 標記快取資料,並在變異後使用伺服器操作中的 `updateTag` 進行立即更新,或者在可接受延遲更新的情況下使用 `revalidateTag` 重新驗證。

使用 `updateTag`

當您需要立即在同一請求中使快取資料過期並重新整理時,請使用 `updateTag`

app/actions.ts
import { cacheTag, updateTag } from 'next/cache'
 
export async function getCart() {
  'use cache'
  cacheTag('cart')
  // fetch data
}
 
export async function updateCart(itemId: string) {
  'use server'
  // write data using the itemId
  // update the user cart
  updateTag('cart')
}

使用 `revalidateTag`

當您只想使正確標記的快取條目失效,並採用陳舊資料重新驗證策略時,請使用 `revalidateTag`。這非常適合可以容忍最終一致性的靜態內容。

app/actions.ts
import { cacheTag, revalidateTag } from 'next/cache'
 
export async function getPosts() {
  'use cache'
  cacheTag('posts')
  // fetch data
}
 
export async function createPost(post: FormData) {
  'use server'
  // write data using the FormData
  revalidateTag('posts', 'max')
}

有關更詳細的解釋和用法示例,請參閱 `use cache` API 參考

啟用快取元件

您可以透過在 Next 配置檔案中新增 `cacheComponents` 選項來啟用快取元件(包括 PPR)

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

對路由分段配置的影響

啟用快取元件後,一些路由分段配置選項不再需要或受支援。以下是更改內容以及如何遷移

`dynamic = "force-dynamic"`

**不需要。** 啟用快取元件後,所有頁面預設都是動態的,因此此配置是不必要的。

// Before - No longer needed
export const dynamic = 'force-dynamic'
 
export default function Page() {
  return <div>...</div>
}
// After - Just remove it, pages are dynamic by default
export default function Page() {
  return <div>...</div>
}

`dynamic = "force-static"`

**替換為 `use cache`。** 您必須將 `use cache` 新增到相關路由的每個佈局和頁面中。

注意:`force-static` 以前允許使用 `cookies()` 等執行時 API,但現在不再支援。如果您新增 `use cache` 並看到與執行時資料相關的錯誤,則必須刪除執行時 API 的使用。

// Before
export const dynamic = 'force-static'
 
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}
// After - Use 'use cache' instead
export default async function Page() {
  'use cache'
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}

`revalidate`

**替換為 `cacheLife`。** 使用 `cacheLife` 函式來定義快取持續時間,而不是路由分段配置。

// Before
export const revalidate = 3600 // 1 hour
 
export default async function Page() {
  return <div>...</div>
}
// After - Use cacheLife
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  return <div>...</div>
}

`fetchCache`

**不需要。** 使用 `use cache`,快取範圍內的所有資料獲取都會自動快取,使 `fetchCache` 不再需要。

// Before
export const fetchCache = 'force-cache'
// After - Use 'use cache' to control caching behavior
export default async function Page() {
  'use cache'
  // All fetches here are cached
  return <div>...</div>
}

快取元件之前與之後

瞭解快取元件如何改變您的思維模式

快取元件之前

  • **預設靜態**:Next.js 嘗試為您預渲染和快取儘可能多的內容,除非您選擇退出
  • **路由級別控制**:`dynamic`、`revalidate`、`fetchCache` 等開關控制整個頁面的快取
  • **`fetch` 的限制**:單獨使用 `fetch` 是不完整的,因為它不涵蓋直接資料庫客戶端或其他伺服器端 IO。巢狀的 `fetch` 切換到動態(例如 ` { cache: 'no-store' }`)可能會無意中改變整個路由行為

使用快取元件

  • **預設動態**:所有內容預設都是動態的。您可以透過在需要的地方新增 `use cache` 來決定快取哪些部分
  • **細粒度控制**:檔案/元件/函式級別的 `use cache``cacheLife` 精確控制您需要的快取
  • **流式傳輸保持不變**:使用 `<Suspense>` 或 `loading.(js|tsx)` 檔案在 shell 立即顯示的同時流式傳輸動態部分
  • **超越 `fetch`**:使用 `use cache` 指令,快取可以應用於所有伺服器 IO(資料庫呼叫、API、計算),而不僅僅是 `fetch`。巢狀的 `fetch` 呼叫不會悄悄地翻轉整個路由,因為行為由顯式快取邊界和 `Suspense` 控制

示例

動態 API

當訪問 `cookies()` 等執行時 API 時,Next.js 只會預渲染此元件上方的回退 UI。

在此示例中,我們沒有定義回退,因此 Next.js 顯示一個錯誤,指示我們提供一個。`<User />` 元件需要包裝在 `Suspense` 中,因為它使用了 `cookies` API

app/user.tsx
import { cookies } from 'next/headers'
 
export async function User() {
  const session = (await cookies()).get('session')?.value
  return '...'
}

現在我們的使用者元件周圍有一個 `Suspense` 邊界,我們可以使用骨架 UI 預渲染頁面,並在特定使用者發出請求時流式傳輸 `<User />` UI

app/page.tsx
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'
 
export default function Page() {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<AvatarSkeleton />}>
        <User />
      </Suspense>
    </section>
  )
}

傳遞動態 props

元件僅在訪問值時才選擇動態渲染。例如,如果您從 `<Page />` 元件讀取 `searchParams`,您可以將此值作為 prop 轉發給另一個元件

app/page.tsx
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'
 
export default function Page({
  searchParams,
}: {
  searchParams: Promise<{ sort: string }>
}) {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<TableSkeleton />}>
        <Table searchParams={searchParams.then((search) => search.sort)} />
      </Suspense>
    </section>
  )
}

在表格元件內部,訪問 `searchParams` 中的值將使元件動態化,而頁面的其餘部分將被預渲染。

app/table.tsx
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
  const sort = (await sortPromise) === 'true'
  return '...'
}

常見問題

這會取代部分預渲染 (PPR) 嗎?

不。快取元件**實現了** PPR 作為一項功能。舊的實驗性 PPR 標誌已被移除,但 PPR 將繼續存在。

PPR 提供了靜態 shell 和流式傳輸基礎設施;`use cache` 允許您在有益時將最佳化的動態輸出包含在該 shell 中。

我應該首先快取什麼?

您快取的內容應該取決於您希望 UI 載入狀態如何。如果資料不依賴於執行時資料,並且您接受在一段時間內為多個請求提供快取值,請使用帶有 `cacheLife` 的 `use cache` 來描述該行為。

對於具有更新機制的內容管理系統,請考慮使用具有更長快取持續時間的標籤,並依賴 `revalidateTag` 將靜態初始 UI 標記為已準備好重新驗證。此模式允許您提供快速的快取響應,同時在內容實際更改時更新內容,而不是搶先使快取過期。

如何快速更新快取內容?

使用 `cacheTag` 標記您的快取資料,然後觸發 `updateTag``revalidateTag`