如何使用部分預渲染 (Partial Prerendering)

部分預渲染 (Partial Prerendering, PPR) 是一種渲染策略,允許您在同一路由中結合靜態與動態內容。這能提升初始頁面效能,同時仍支援個人化的動態資料。

部分預渲染的產品頁面,顯示靜態導覽列與產品資訊,以及動態購物車與推薦商品

當使用者造訪路由時:

  • 伺服器會傳送包含靜態內容的 外殼 (shell),確保快速的初始載入。
  • 外殼會為動態內容預留 空洞 (holes),這些內容將非同步載入。
  • 動態空洞會 並行串流 (streamed in parallel),減少頁面的整體載入時間。

🎥 觀看影片: 為什麼需要 PPR 及其運作原理 → YouTube (10 分鐘)

部分預渲染如何運作?

要理解部分預渲染,需先熟悉 Next.js 提供的渲染策略。

靜態渲染 (Static Rendering)

靜態渲染會預先生成 HTML — 無論是在建置時或透過 重新驗證 (revalidation)。結果會被快取並在所有使用者和請求間共享。

在部分預渲染中,Next.js 會為路由預先渲染一個 靜態外殼 (static shell)。這可包含版面配置 (layout) 和任何不依賴請求時資料的元件。

動態渲染 (Dynamic Rendering)

動態渲染會在 請求時 (request time) 生成 HTML。這讓您能根據請求時資料提供個人化內容。

元件在以下情況會變為動態:

在部分預渲染中,使用這些 API 會拋出特殊的 React 錯誤,告知 Next.js 該元件無法靜態渲染,導致建置錯誤。您可使用 Suspense 邊界包裝元件,延遲渲染至執行階段。

Suspense

React Suspense 用於延遲應用程式部分的渲染,直到滿足某些條件。

在部分預渲染中,Suspense 用於標記元件樹中的 動態邊界 (dynamic boundaries)

在建置時,Next.js 會預先渲染靜態內容和 fallback UI。動態內容會 延後 (postponed) 至使用者請求路由時才處理。

用 Suspense 包裝元件不會使元件本身變為動態(取決於您的 API 使用方式),而是將 Suspense 作為封裝動態內容的邊界,並啟用 串流 (streaming)

app/page.js
import { Suspense } from 'react'
import StaticComponent from './StaticComponent'
import DynamicComponent from './DynamicComponent'
import Fallback from './Fallback'

export const experimental_ppr = true

export default function Page() {
  return (
    <>
      <StaticComponent />
      <Suspense fallback={<Fallback />}>
        <DynamicComponent />
      </Suspense>
    </>
  )
}

串流 (Streaming)

串流將路由拆分為多個區塊 (chunks),並在準備完成時逐步串流至客戶端。這讓使用者能立即看到部分頁面內容,而無需等待整個內容完成渲染。

圖表顯示客戶端上的部分渲染頁面,以及正在串流區塊的載入 UI。

在部分預渲染中,以 Suspense 包裝的動態元件會從伺服器並行串流。

圖表顯示串流期間路由區段的並行化,展示個別區塊的資料獲取、渲染和水合 (hydration)。

為減少網路開銷,完整回應 — 包含靜態 HTML 和串流的動態部分 — 會在 單一 HTTP 請求 中傳送。這避免了額外的往返,提升初始載入和整體效能。

啟用部分預渲染

您可透過在 next.config.ts 檔案中新增 ppr 選項來啟用 PPR:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    ppr: 'incremental',
  },
}

export default nextConfig

'incremental' 值允許您為特定路由採用 PPR:

/app/dashboard/layout.tsx
export const experimental_ppr = true

export default function Layout({ children }: { children: React.ReactNode }) {
  // ...
}
/app/dashboard/layout.js
export const experimental_ppr = true

export default function Layout({ children }) {
  // ...
}

未設定 experimental_ppr 的路由預設為 false,不會使用 PPR 進行預渲染。您需要為每個路由明確啟用 PPR。

須知事項

  • experimental_ppr 會套用至路由區段的所有子項目,包含巢狀版面配置和頁面。您無需在每個檔案中新增,僅需在路由的頂層區段設定。
  • 若要為子區段停用 PPR,可在子區段中將 experimental_ppr 設為 false

範例

動態 API

當使用需檢視傳入請求的動態 API 時,Next.js 會為路由啟用動態渲染。若要繼續使用 PPR,請以 Suspense 包裝元件。例如,<User /> 元件因使用 cookies API 而變為動態:

import { cookies } from 'next/headers'

export async function User() {
  const session = (await cookies()).get('session')?.value
  return '...'
}

<User /> 元件會以串流方式載入,而 <Page /> 內的任何其他內容會被預先渲染並成為靜態外殼的一部分。

import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'

export const experimental_ppr = true

export default function Page() {
  return (
    <section>
      <h1>此內容將被預先渲染</h1>
      <Suspense fallback={<AvatarSkeleton />}>
        <User />
      </Suspense>
    </section>
  )
}

傳遞動態屬性 (props)

元件僅在存取值時才會啟用動態渲染。例如,若您從 <Page /> 元件讀取 searchParams,可將此值作為屬性傳遞至其他元件:

import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'

export default function Page({
  searchParams,
}: {
  searchParams: Promise<{ sort: string }>
}) {
  return (
    <section>
      <h1>此內容將被預先渲染</h1>
      <Suspense fallback={<TableSkeleton />}>
        <Table searchParams={searchParams} />
      </Suspense>
    </section>
  )
}

在表格元件內部,存取 searchParams 的值會使元件變為動態,而頁面其他部分會被預先渲染。

export async function Table({
  searchParams,
}: {
  searchParams: Promise<{ sort: string }>
}) {
  const sort = (await searchParams).sort === 'true'
  return '...'
}

On this page