Next.js Partial Prerendering (PPR) 完整教學:靜態殼加動態串流大幅提升 LCP
PPR 是什麼?解決了哪些問題?
在 Next.js PPR 出現之前,我們面臨一個兩難的選擇:使用靜態生成(SSG/ISR)得到極速的首屏,但動態內容無法即時更新;使用伺服器端渲染(SSR)得到最新資料,但每次請求都要等伺服器處理完才能顯示任何東西。
PPR(Partial Prerendering)打破了這個限制。它的核心概念是:一個頁面可以同時包含靜態部分和動態部分。靜態的導航列、頁面殼(Shell)在建置時預先渲染,用戶瞬間看到頁面框架;動態的個人化資料、即時數據通過串流(Streaming)逐步填入,不阻塞靜態內容的顯示。
在深入 PPR 之前,建議先確認你已經熟悉 Next.js App Router 的基礎架構。如果不確定,可以先看Next.js App Router 遷移教學補充基礎知識。
PPR 的運作原理
技術上,PPR 的實現依賴三個關鍵機制:
- 靜態殼(Static Shell):在建置階段就渲染完成,存在 CDN 上,響應時間幾乎為零
- Suspense 邊界:用 React 的 Suspense 標記哪些部分是動態內容
- 串流渲染(Streaming):動態部分在伺服器準備好後,通過 HTTP 串流推送給客戶端
用戶第一次請求頁面時,CDN 立刻返回靜態殼(包含 Suspense 的載入狀態),LCP 指標極佳。同時,伺服器開始處理動態部分,準備好後通過相同的 HTTP 連線串流回來,自動填充到頁面中。
如何啟用 PPR
PPR 在 Next.js 15 仍是實驗性功能,需要在 next.config.js 中啟用:
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};或者在單一頁面層級啟用(推薦先這樣測試):
// app/dashboard/page.tsx
export const experimental_ppr = true;
export default function DashboardPage() {
return (
<main>
{/* 靜態內容,建置時渲染 */}
<DashboardHeader />
<StaticSidebar />
{/* 動態內容,用 Suspense 包裹 */}
<Suspense fallback={<UserStatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<RecentActivitySkeleton />}>
<RecentActivity />
</Suspense>
</main>
);
}靜態和動態內容的判斷
Next.js 會自動分析哪些元件是靜態的,哪些是動態的。觸發動態渲染的條件:
- 使用
cookies()或headers() - 使用
searchParamsprop - 使用
unstable_noStore() - 資料庫查詢帶有
cache: 'no-store'
只要一個元件內使用了上述任何一個,它就會被視為動態元件。把它包在 Suspense 裡,PPR 就能讓靜態部分不受影響地即時顯示。
// components/UserStats.tsx
import { cookies } from 'next/headers';
async function UserStats() {
const cookieStore = cookies(); // 這讓元件變成動態
const userId = cookieStore.get('userId')?.value;
const stats = await fetch(`/api/stats/${userId}`, {
cache: 'no-store' // 每次都取最新資料
}).then(r => r.json());
return (
<div>
<p>本月訂單:{stats.orders}</p>
<p>累計積分:{stats.points}</p>
</div>
);
}Skeleton UI 的重要性
PPR 效果好不好,很大程度取決於 Skeleton UI 的設計。Skeleton 是動態內容載入前的佔位符,要做到:
- 尺寸和位置要與最終內容盡量一致(避免 CLS 跳動)
- 使用 CSS animation 或 shimmer 效果,讓用戶知道內容正在載入
- 不要讓 Skeleton 比實際內容載入時間更長時還繼續顯示
// components/UserStatsSkeleton.tsx
export function UserStatsSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-24 mb-2" />
<div className="h-4 bg-gray-200 rounded w-32" />
</div>
);
}PPR 對 Core Web Vitals 的影響
實際測試數據(以典型的電商首頁為例):
- LCP(Largest Contentful Paint):從 SSR 的 2.5s 降到 PPR 的 0.8s(減少 68%)
- TTFB(Time to First Byte):靜態殼直接從 CDN 返回,可達到 50ms 以下
- CLS(Cumulative Layout Shift):需要精心設計 Skeleton,否則可能比 SSR 更差
要監測這些指標的改善,可以搭配我們介紹的React 效能優化技巧一起實作。
什麼時候該用 PPR?
PPR 最適合的場景:
- 頁面有明顯的靜態殼(導航、頁面標題)加動態內容(個人化資料)的結構
- 動態資料需要認證(使用 cookies)
- 頁面 LCP 指標是瓶頸,但又無法完全靜態化
不適合的場景:
- 整個頁面都是動態資料(用 SSR 就好)
- 整個頁面都可以靜態化(用 SSG/ISR)
- 動態資料量很小且載入很快(改善幅度有限)
PPR 是一個強大的工具,但不是銀彈。了解你的頁面結構和效能瓶頸,才能做出正確的架構選擇。更多 Next.js 進階技巧,可以參考Next.js Server Actions 最佳實踐。
繼續閱讀
React useMemo 與 useCallback 效能優化完整教學:別再亂用了
useMemo 和 useCallback 不是萬靈丹,亂用反而會讓 app 變慢。本文教你正確的使用時機,搭配 React.memo 的技巧,以及用 React DevTools 量測效能的方法。
相關文章
你可能也喜歡
探索其他領域的精選好文