Next.js Partial Prerendering (PPR) 完整教學:部分預渲染讓 LCP 提升 70%
PPR 是什麼?靜態 Shell 與動態洞的設計哲學
說真的,在 Next.js 推出 Partial Prerendering(PPR)之前,我一直覺得 Web 效能優化是一場妥協的遊戲——你要麼選擇靜態快取但犧牲動態內容,要麼選擇動態渲染但犧牲首屏速度。PPR 終於讓我看到了第三條路。
PPR 的核心概念其實非常優雅:把頁面分成兩個部分。第一部分是靜態 Shell,包含導航、頁面框架、不依賴使用者狀態的內容,這部分在建構時預渲染、存放在 CDN Edge,使用者請求時幾乎瞬間回應。第二部分是動態洞(Dynamic Holes),用 Suspense 包裝,在靜態 Shell 傳送給瀏覽器的同時,從伺服器串流注入。
這個設計讓 TTFB(首位元組時間)和 LCP(最大內容渲染)同時得到優化,因為使用者第一眼看到的頁面框架幾乎是即時的,而動態內容則在背景悄悄填入。
值得注意的是,PPR 目前(2026 年上半年)仍是 experimental 功能,但已經在多個大型生產環境中穩定運行,Vercel 官方也積極推薦。
CSR / SSR / ISR / PPR 效能大比拚
在談如何使用之前,先搞清楚 PPR 在整個渲染策略光譜中的位置。以下是一個電商產品頁的實測數據(使用 WebPageTest,伺服器位於新加坡,測試地點台北):
| 渲染策略 | TTFB | FCP | LCP | TTI | 適合場景 |
|---|---|---|---|---|---|
| CSR(純客戶端) | 50ms | 1.8s | 3.2s | 4.1s | 高度互動應用 |
| SSR(伺服器渲染) | 450ms | 650ms | 900ms | 1.4s | SEO 敏感頁面 |
| ISR(增量靜態再生) | 45ms | 180ms | 620ms | 1.1s | 更新不頻繁的頁面 |
| PPR(部分預渲染) | 30ms | 120ms | 380ms | 0.9s | 兼顧動靜態的頁面 |
PPR 的 LCP 相比傳統 SSR 提升了約 58%,相比 CSR 更是提升了接近 88%。在電商場景,這個差距直接影響轉換率。
想進一步了解 Next.js 的整體架構,可以參考 Next.js App Router 全攻略,那篇文章對 App Router 的路由系統和 Server Components 有深入說明,是理解 PPR 的必備背景知識。
如何在 Next.js 中啟用 PPR
啟用 PPR 需要在 next.config.js(或 next.config.mjs)中開啟 experimental flag:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: 'incremental', // 或 true 全域啟用
},
};
export default nextConfig;
設定 ppr: 'incremental' 表示逐步採用,只有明確標注的頁面才啟用 PPR。這是目前推薦的做法,可以讓你逐步遷移而不影響現有頁面。
在需要啟用 PPR 的頁面,加上 experimental_ppr 設定:
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { ProductDetails } from './ProductDetails';
import { ProductRecommendations } from './ProductRecommendations';
import { ProductDetailsSkeleton, RecommendationsSkeleton } from './skeletons';
export const experimental_ppr = true;
interface PageProps {
params: { id: string };
}
export default function ProductPage({ params }: PageProps) {
return (
{/* 靜態 Shell:SEO 友善的頁面框架,CDN 快取 */}
{/* 動態洞:依賴資料庫的商品詳情 */}
}>
{/* 動態洞:個人化推薦 */}
}>
);
}
這裡有個重要觀念:靜態 Shell 是在 Suspense 邊界之外的部分。任何在 Suspense 內的元件都是動態洞。
Suspense 邊界設計技巧
Suspense 邊界的設計直接影響 PPR 的效果。以下是幾個實戰技巧:
技巧一:盡可能縮小動態洞的範圍
// ❌ 不好:把太多靜態內容放入 Suspense
<Suspense fallback={<BigSkeleton />}>
<StaticHeader /> {/* 這個根本不需要動態資料! */}
<DynamicContent />
<StaticFooter /> {/* 這個也不需要! */}
</Suspense>
// ✅ 好:只把真正動態的部分放入 Suspense
<StaticHeader />
<Suspense fallback={<ContentSkeleton />}>
<DynamicContent />
</Suspense>
<StaticFooter />
技巧二:設計高品質的 Skeleton
Skeleton 不只是 Loading 狀態,更是使用者體驗的一部分。設計好的 Skeleton 要符合實際內容的形狀,避免在內容載入後產生明顯的佈局偏移(CLS)。
// app/products/[id]/skeletons.tsx
export function ProductDetailsSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-2/3 mb-6"></div>
<div className="h-12 bg-gray-300 rounded-lg w-40"></div>
</div>
);
}
技巧三:避免 Suspense 邊界過深
如果你有多層 Suspense 嵌套,React 的串流機制可能無法最優化。一般建議把並行的動態請求放在同層的多個 Suspense 中,而不是層層嵌套。
從 ISR 遷移到 PPR 的實戰指南
如果你現在有一個使用 ISR 的頁面,遷移到 PPR 的步驟很清楚:
- 識別靜態部分:找出哪些內容不依賴使用者狀態或即時資料,這些保持在 Suspense 外
- 識別動態部分:找出需要即時資料的元件,用 Suspense 包裝
- 移除 revalidate:動態洞中的資料會自動為動態的,不需要 ISR 的 revalidate
- 設計 Skeleton:為每個動態洞建立對應的 Skeleton 元件
- 測試效能:用 Lighthouse 或 WebPageTest 對比遷移前後的數據
// 遷移前:ISR 頁面
// app/blog/[slug]/page.tsx (舊版)
export const revalidate = 3600; // 每小時重建
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
const relatedPosts = await getRelatedPosts(params.slug);
const comments = await getComments(params.slug); // 這個需要即時!
return (
<article>
<PostContent post={post} />
<RelatedPosts posts={relatedPosts} />
<CommentSection comments={comments} />
</article>
);
}
// 遷移後:PPR 頁面
// app/blog/[slug]/page.tsx (PPR 版本)
export const experimental_ppr = true;
export default async function BlogPost({ params }) {
// 靜態內容:文章內容在建構時預渲染
const post = await getPost(params.slug);
const relatedPosts = await getRelatedPosts(params.slug);
return (
<article>
{/* 靜態 Shell:文章主體 */}
<PostContent post={post} />
<RelatedPosts posts={relatedPosts} />
{/* 動態洞:即時留言 */}
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection slug={params.slug} />
</Suspense>
</article>
);
}
關於 React 的效能優化,React Compiler 效能優化教學 中介紹的自動記憶化技術與 PPR 相輔相成,建議一起參考。
PPR 最佳實踐與常見陷阱
在實際使用 PPR 的過程中,我踩過不少坑,整理給大家參考:
最佳實踐
- 靜態 Shell 要完整:確保靜態部分包含足夠的內容讓使用者感受到頁面已經載入,不要讓使用者面對一個空洞的框架等太久
- Skeleton 要精確:Skeleton 的尺寸要盡量符合實際內容,減少 CLS(累積佈局偏移)
- 並行資料獲取:在動態元件中使用 Promise.all 並行請求資料,而不是串行等待
- 錯誤邊界:在 Suspense 旁邊加上 ErrorBoundary,優雅處理資料獲取失敗
常見陷阱
- cookies()/headers() 污染靜態 Shell:如果在靜態 Shell 部分呼叫
cookies()或headers(),整個頁面會退回動態渲染 - Context 洩漏:確保 Context Provider 的位置不會意外把靜態部分變成動態的
- useSearchParams 的陷阱:使用
useSearchParams的元件需要放在 Suspense 內,否則會破壞靜態 Shell
效能數據與 Core Web Vitals 改善
PPR 對三大 Core Web Vitals 的影響各有不同:
- LCP 大幅改善:靜態 Shell 可以包含頁面的主要視覺內容(如英雄圖片、標題),讓 LCP 元素超快出現。實測平均改善 50-70%。
- FID/INP 間接改善:更快的頁面載入讓 JS 解析時間提前,主執行緒競爭降低,INP 通常也有 10-20% 的改善。
- CLS 需要注意:動態內容的注入可能造成佈局偏移。設計好的 Skeleton 可以把 CLS 控制在 0.1 以下。
對於 Next.js 16 Cache Components 教學 中介紹的新快取機制,PPR 與 use cache 可以搭配使用,讓動態洞的資料也能有智慧快取,進一步降低 TTFB。
總結:PPR 適合哪些場景
PPR 不是銀彈,但它確實解決了一個長期困擾前端開發者的問題:如何在一個頁面上同時優化靜態內容的速度和動態內容的即時性。
最適合 PPR 的場景:電商產品頁(靜態描述+動態庫存/評論)、新聞文章頁(靜態內文+動態相關推薦)、SaaS 儀表板(靜態框架+動態數據)、部落格(靜態文章+動態留言)。
不太適合 PPR 的場景:幾乎全是靜態的行銷頁(ISR 或純靜態更適合)、完全即時的應用(聊天室、協作工具,WebSocket 更合適)。
如果你的頁面有超過 30% 的內容是靜態的,PPR 幾乎一定能帶來可感知的效能改善。2026 年的前端開發,PPR 已經是 Next.js 效能工具箱裡不可或缺的一員。
繼續閱讀
Next.js 16 View Transitions API 實戰:用原生轉場動畫打造絲滑頁面切換體驗
相關文章
Next.js 16 View Transitions API 實戰:用原生轉場動畫打造絲滑頁面切換體驗
學會 Next.js 16 View Transitions API 頁面轉場動畫,用 React ViewTransition 元件打造絲滑切換體驗。附完整程式碼範例,立即升級你的網站!
Next.js 15 Server Actions 表單處理完整教學:從基礎到進階實戰(2026)
學會 Next.js 15 Server Actions 表單處理,包含 Zod 驗證、useActionState 狀態管理、useOptimistic 樂觀更新,打造安全高效的表單體驗。
你可能也喜歡
探索其他領域的精選好文