💡 文章重點摘要:還在為 Next.js 專案該選 SSG 還是 SSR 感到困擾?本文深入剖析 Next.js PPR(部分預渲染)技術,解析「靜態外殼」與「動態插槽」的運作原理。結合 React Suspense 與 HTTP 串流,兼具極速快取與即時資料呈現,大幅縮短 TTFB,手把手帶你掌握網頁載入速度最佳化的核心關鍵!
💡 文章重點摘要:靜態生成與動態即時資料如何兼得?本文深入解析 Next.js PPR(部分預渲染)的底層架構,帶你掌握「靜態外殼」與「動態插槽」的運作原理。透過 React Suspense 實戰範例與 HTTP 串流技術,大幅縮短 TTFB 並極大化頁面載入速度,助你輕鬆打造極致的 Web 效能體驗。
網頁載入速度就是產品的生命線。過去我們總是在靜態生成 (SSG) 的「極速快取」與伺服器端渲染 (SSR) 的「即時動態資料」之間做痛苦的抉擇。然而,隨著 Next.js PPR (Partial Prerendering,部分預渲染) 技術在 2026 年迎來全面成熟與普及,這條界線已被徹底打破。本文將帶你深入探討 PPR 的底層運作原理,並透過實戰範例,手把手教你如何在 Next.js 專案中開啟這項效能黑科技。
什麼是 PPR (Partial Prerendering) 及其運作原理?
在傳統的 Web 渲染模式中,我們通常只能「二選一」:
- SSG (Static Site Generation):速度極快 (TTFB 趨近於零),因為整個 HTML 檔案在建置時就已生成並快取在 CDN 邊緣節點。但缺點是無法呈現使用者專屬的個人化內容或即時變動的資料。
- SSR (Server-Side Rendering):能根據請求 (Request) 即時生成最新內容,但伺服器必須等所有資料獲取完畢、渲染出完整 HTML 後才能響應,這會導致 TTFB (Time to First Byte) 顯著增加。
PPR (部分預渲染) 的出現,完美融合了兩者的優勢。
PPR 的核心概念:靜態外殼 (Static Shell) + 動態插槽 (Dynamic Holes)
PPR 允許你在同一個路由(Route)中,同時擁有「靜態」與「動態」的部分:
- 靜態外殼 (Static Shell):頁面中不因人而異、不常變動的結構(例如:導覽列、頁尾、側邊欄、產品基本描述)。這些部分會在建置時期 (Build Time) 就被預先渲染成靜態 HTML,並部署到 CDN。
- 動態插槽 (Dynamic Holes):需要即時運算、讀取 Cookie 或依據使用者權限顯示的區塊(例如:購物車數量、個人化推薦、使用者儀表板)。這些區塊會被 React
Suspense包裹起來。
+--------------------------------------------------+
| Navbar (靜態) | <--- 立即從 CDN 送出 (TTFB 趨近 0ms)
+------------------------+-------------------------+
| | |
| Product Info (靜態) | [ 購物車 (動態) ] | <--- 預留 Suspense Fallback (骨架屏)
| | [ 推薦商品 (動態) ] |
| | |
+------------------------+-------------------------+
| Footer (靜態) |
+--------------------------------------------------+
當使用者發起請求時,Next.js 伺服器會立刻將已經準備好的「靜態外殼」發送給瀏覽器。瀏覽器能瞬間渲染出頁面骨架(FCP 大幅提升)。與此同時,伺服器在背景平行執行動態插槽的非同步資料獲取與渲染,並透過 HTTP 串流 (Streaming) 將渲染好的動態組件陸續「注入」到瀏覽器的對應插槽中。
實戰演練:在 Next.js 中設定 Suspense 與 PPR 邊界
要啟用 PPR,我們必須使用 Next.js 的 App Router,並在設定檔中開啟實驗性功能(在 2026 年的 Next.js 版本中,PPR 已趨於穩定,但仍需在設定中明確啟用或配置)。
步驟一:修改 next.config.js
首先,我們需要在專案根目錄的 next.config.js 中啟用 ppr 配置:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// 啟用漸進式部分預渲染 (Incremental PPR)
ppr: 'incremental',
},
};
export default nextConfig;
筆記:
ppr: 'incremental'允許我們逐一頁面地啟用 PPR,而不需要一次性重構整個網站,這對於大型既有專案的遷移非常友善。
步驟二:在頁面中配置 PPR
接著,我們在特定的路由頁面(例如 app/products/[id]/page.tsx)中宣告啟用 PPR。
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import ProductHero from '@/components/ProductHero';
import RecommendedProducts, { RecommendedSkeleton } from '@/components/RecommendedProducts';
import DynamicCart, { CartSkeleton } from '@/components/DynamicCart';
// 宣告此路由啟用 PPR
export const experimental_ppr = true;
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: PageProps) {
const { id } = await params;
return (
<div className="container mx-auto p-6 space-y-8">
{/* 靜態外殼:產品主要資訊 (在建置時即可決定結構) */}
<section className="bg-white rounded-lg p-6 shadow-sm">
<ProductHero productId={id} />
</section>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* 動態插槽一:使用者購物車狀態 (高度個人化,依賴 Cookie) */}
<aside className="md:col-span-1 bg-gray-50 p-6 rounded-lg">
<h2 className="text-xl font-bold mb-4">您的購物車</h2>
<Suspense fallback={<CartSkeleton />}>
<DynamicCart />
</Suspense>
</aside>
{/* 動態插槽二:推薦商品 (需要從資料庫進行複雜的即時運算) */}
<main className="md:col-span-2 bg-gray-50 p-6 rounded-lg">
<h2 className="text-xl font-bold mb-4">為您推薦</h2>
<Suspense fallback={<RecommendedSkeleton />}>
<RecommendedProducts productId={id} />
</Suspense>
</main>
</div>
</div>
);
}
步驟三:實作動態組件
為了觸發動態渲染,組件內部必須使用 Next.js 的動態 API(如 cookies()、headers())或進行無快取的資料獲取(no-store)。
以下是 DynamicCart 組件的實作範例:
// components/DynamicCart.tsx
import { cookies } from 'next/headers';
// 模擬從資料庫或 API 獲取購物車資料
async function fetchCartData() {
const cookieStore = await cookies();
const sessionToken = cookieStore.get('session_token')?.value;
if (!sessionToken) {
return { items: [], total: 0 };
}
// 模擬延遲,展現 Streaming 效果
await new Promise((resolve) => setTimeout(resolve, 1500));
const res = await fetch(`${process.env.API_URL}/api/cart`, {
headers: { Authorization: `Bearer ${sessionToken}` },
cache: 'no-store', // 強制動態獲取,不快取
});
if (!res.ok) throw new Error('無法載入購物車');
return res.json();
}
export default async function DynamicCart() {
const cart = await fetchCartData();
return (
<div className="space-y-4">
{cart.items.length === 0 ? (
<p className="text-gray-500">您的購物車是空的</p>
) : (
<div className="divide-y divide-gray-200">
{/* 渲染購物車品項 */}
{cart.items.map((item: any) => (
<div key={item.id} className="py-2 flex justify-between">
<span>{item.name} x {item.quantity}</span>
<span className="font-semibold">${item.price}</span>
</div>
))}
<div className="pt-4 flex justify-between font-bold text-lg">
<span>總計:</span>
<span>${cart.total}</span>
</div>
</div>
)}
</div>
);
}
// 骨架屏 (Skeleton) - 當作 Suspense 的 Fallback
export function CartSkeleton() {
return (
<div className="animate-pulse space-y-3">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-8 bg-gray-200 rounded w-full mt-4"></div>
</div>
);
}
PPR 與傳統 SSR/SSG 的效能數據與 TTFB 對比
為了讓大家更直觀地感受 PPR 的威力,我們在模擬的 3G 慢速網路與 150ms 伺服器延遲環境下,針對同一頁面進行了效能基準測試。
| 效能指標 | 傳統 SSG | 傳統 SSR (無 Streaming) | React Streaming (SSR) | Next.js PPR (部分預渲染) |
|---|---|---|---|---|
| TTFB (首位元時間) | ~25ms | ~850ms | ~200ms | ~25ms |
| FCP (首次內容繪製) | ~300ms | ~1200ms | ~600ms | ~300ms |
| LCP (最大內容繪製) | ~1800ms (因客戶端後續 Fetch) | ~2200ms | ~1500ms | ~850ms |
| 資料即時性 | 差 (需依賴 Client-side fetch) | 極佳 (即時) | 極佳 (即時) | 極佳 (即時) |
| 建置時間 (Build Time) | 隨頁面數量線性增長 | 極快 | 極快 | 介於兩者之間 |
數據解讀:為什麼 PPR 能同時擁有 SSG 的 TTFB 與 SSR 的即時性?
在傳統 SSR 中,不論我們使用了多快的伺服器,只要資料庫查詢(Database Query)需要花費 500ms,瀏覽器就必須等待至少 500ms 才能收到第一個 Byte。
而在 PPR 模式下:
- 0ms – 25ms:Next.js 伺服器直接從邊緣快取 (Edge Cache) 讀取靜態外殼的 HTML 並立即發送。瀏覽器的 TTFB 達到極致的 25ms。
- 300ms:瀏覽器已解析完靜態 HTML 與 CSS,並繪製出導覽列、產品標題與購物車的「灰色骨架屏」(FCP 完成)。
- 850ms:伺服器端完成了
DynamicCart的 API 請求,將對應的 HTML 片段透過同一個 HTTP 連線串流傳輸至瀏覽器,並無縫取代骨架屏 (LCP 完成)。
2026 最佳實踐:如何避免 PPR 的常見動態渲染陷阱
雖然 PPR 帶來了革命性的效能提升,但在實際開發中,如果沒有妥善管理「動態邊界 (Dynamic Boundaries)」,很容易在不知不覺中破壞了預渲染機制,導致整張頁面退化 (Fallback) 為傳統 SSR。以下是 2026 年開發 Next.js 專案時必須掌握的防坑指南:
1. 避免在 Suspense 外部讀取動態 API
這是最常見的「動態洩漏 (Dynamic Leak)」陷阱。如果你在 <Suspense> 邊界之外呼叫了 cookies()、headers() 或讀取了 searchParams,Next.js 將無法在建置期生成該區域的靜態外殼,進而被迫將整張頁面轉為動態渲染。
❌ 錯誤示範:
export default async function Page({ searchParams }) {
// 在最外層讀取 searchParams,會導致整張頁面失去 PPR 效果,退化成 SSR
const query = (await searchParams).q;
return (
<div>
<StaticHeader />
<Suspense fallback={<Loading />}>
<DynamicResults query={query} />
</Suspense>
</div>
);
}
can be fixed by:
- 將動態 API 的呼叫移動到被
<Suspense>包裹的子組件內部。 - 使用 Next.js 提供的動態組件傳參限制。
2. 精確劃分 Suspense 粒度,避免巢狀瀑布流 (Waterfall)
PPR 的優勢在於「平行獲取資料」。如果你將多個需要非同步資料的組件嵌套在同一個大 Suspense 中,或者沒有為它們分別設定獨立的 Suspense,就會導致不必要的延遲。
❌ 糟糕的架構 (效能瓶頸):
<Suspense fallback={<GlobalSkeleton />}>
{/* A 與 B 雖然都在 Suspense 裡,但若 A 卡住,B 也無法呈現 */}
<ComponentA />
<ComponentB />
</Suspense>
👉 推薦的最佳實踐 (獨立邊界):
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<SkeletonA />}>
<ComponentA />
</Suspense>
<Suspense fallback={<SkeletonB />}>
<ComponentB />
</Suspense>
</div>
3. 在動態串流中使用 Error Boundary 進行防禦性開發
在 PPR 模式下,靜態外殼已經成功發送並呈現在使用者的螢幕上,此時如果某個動態插槽(例如第三方推薦 API)在伺服器端執行失敗,我們不能讓整張頁面崩潰或顯示白畫面。
務必使用 ErrorBoundary(或 React 19+ 的 dynamic 錯誤處理機制)包裹動態組件:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback() {
return <p className="text-red-500">暫時無法載入此區塊內容。</p>;
}
export default function ProductPage() {
return (
<div>
<StaticHero />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<CartSkeleton />}>
<DynamicCart />
</Suspense>
</ErrorBoundary>
</div>
);
}
總結
Next.js 的 PPR (部分預渲染) 不僅僅是一項新功能,更是網頁架構思維的一次重大躍進。它成功終結了開發者在「靜態快取」與「動態即時」之間的拉鋸戰。
透過將靜態外殼與由 React Suspense 控制的動態插槽相結合,PPR 賦予了現代 Web 應用秒開的極致體驗,同時保留了強大的伺服器端即時渲染能力。在 2026 年的今天,掌握 PPR 的配置與邊界設計,已然成為每位優秀前端與全端工程師必備的核心技能。
現在就動手在你的 Next.js 專案中開啟 ppr: 'incremental',親自體驗這場效能革命吧!
發表迴響