什么是部分预渲染(Partial Prerendering - PPR)?
PPR 概述
Partial Prerendering (PPR) 是 Next.js 15 的实验性功能,它结合了静态生成 (SSG) 和动态内容,允许在同一个页面中同时使用静态和动态渲染。
1. 基本概念
// app/page.js
import { Suspense } from 'react'
export default function HomePage() {
return (
<div>
{/* 静态内容 - 预渲染 */}
<header>
<h1>Welcome to My App</h1>
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent />
</Suspense>
{/* 静态内容 - 预渲染 */}
<footer>
<p>© 2024 My App</p>
</footer>
</div>
)
}
async function DynamicContent() {
// 动态数据获取
const data = await fetch('https://api.example.com/data')
const result = await data.json()
return (
<div>
<h2>Dynamic Content</h2>
<p>{result.message}</p>
</div>
)
}
2. 工作原理
// PPR 工作流程
1. 静态部分预渲染
├── 头部导航
├── 页脚
└── 静态内容
2. 动态部分流式渲染
├── 用户数据
├── 实时内容
└── 个性化内容
3. 客户端水合
├── 静态部分立即显示
├── 动态部分逐步加载
└── 最终完整页面
实际应用示例
1. 博客文章页面
// app/blog/[slug]/page.js
import { Suspense } from 'react'
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const { slug } = params
// 静态内容 - 预渲染
return (
<article>
<header>
<h1>Blog Post</h1>
<nav>
<a href="/blog">← Back to Blog</a>
</nav>
</header>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading post...</div>}>
<PostContent slug={slug} />
</Suspense>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments slug={slug} />
</Suspense>
{/* 静态内容 - 预渲染 */}
<footer>
<p>Published on {new Date().toLocaleDateString()}</p>
</footer>
</article>
)
}
async function PostContent({ slug }) {
const post = await fetch(`https://api.example.com/posts/${slug}`)
if (!post.ok) {
notFound()
}
const data = await post.json()
return (
<div>
<h2>{data.title}</h2>
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
)
}
async function Comments({ slug }) {
const comments = await fetch(`https://api.example.com/posts/${slug}/comments`)
const data = await comments.json()
return (
<section>
<h3>Comments ({data.length})</h3>
{data.map((comment) => (
<div key={comment.id}>
<p>{comment.content}</p>
<small>By {comment.author}</small>
</div>
))}
</section>
)
}
2. 电商产品页面
// app/products/[id]/page.js
import { Suspense } from 'react'
export default async function ProductPage({ params }) {
const { id } = params
return (
<div>
{/* 静态内容 - 预渲染 */}
<header>
<h1>Product Details</h1>
<nav>
<a href="/products">← Back to Products</a>
</nav>
</header>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading product...</div>}>
<ProductInfo id={id} />
</Suspense>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading reviews...</div>}>
<ProductReviews id={id} />
</Suspense>
{/* 动态内容 - 流式渲染 */}
<Suspense fallback={<div>Loading recommendations...</div>}>
<ProductRecommendations id={id} />
</Suspense>
{/* 静态内容 - 预渲染 */}
<footer>
<p>Need help? Contact support</p>
</footer>
</div>
)
}
async function ProductInfo({ id }) {
const product = await fetch(`https://api.example.com/products/${id}`)
const data = await product.json()
return (
<div>
<h2>{data.name}</h2>
<p>Price: ${data.price}</p>
<p>{data.description}</p>
</div>
)
}
async function ProductReviews({ id }) {
const reviews = await fetch(`https://api.example.com/products/${id}/reviews`)
const data = await reviews.json()
return (
<section>
<h3>Reviews ({data.length})</h3>
{data.map((review) => (
<div key={review.id}>
<p>{review.content}</p>
<small>Rating: {review.rating}/5</small>
</div>
))}
</section>
)
}
async function ProductRecommendations({ id }) {
const recommendations = await fetch(
`https://api.example.com/products/${id}/recommendations`
)
const data = await recommendations.json()
return (
<section>
<h3>Recommended Products</h3>
{data.map((product) => (
<div key={product.id}>
<h4>{product.name}</h4>
<p>${product.price}</p>
</div>
))}
</section>
)
}
配置和启用
1. 启用 PPR
// next.config.js
const nextConfig = {
experimental: {
ppr: true,
},
}
module.exports = nextConfig
2. 页面级别配置
// app/page.js
export const experimental_ppr = true
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent />
</Suspense>
</div>
)
}
性能优势
1. 更快的首屏渲染
// 传统 SSR
// 1. 等待所有数据
// 2. 渲染完整页面
// 3. 发送到客户端
// 4. 客户端水合
// PPR
// 1. 预渲染静态部分
// 2. 立即发送到客户端
// 3. 流式渲染动态部分
// 4. 逐步水合
2. 更好的用户体验
// app/dashboard/page.js
export default function Dashboard() {
return (
<div>
{/* 立即显示 */}
<header>
<h1>Dashboard</h1>
<nav>...</nav>
</header>
{/* 逐步加载 */}
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
<Suspense fallback={<div>Loading charts...</div>}>
<Charts />
</Suspense>
<Suspense fallback={<div>Loading recent activity...</div>}>
<RecentActivity />
</Suspense>
</div>
)
}
最佳实践
1. 合理使用 Suspense
// 好的做法 - 细粒度 Suspense
export default function Page() {
return (
<div>
<header>...</header>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
</div>
)
}
// 避免 - 过大的 Suspense 边界
export default function Page() {
return (
<div>
<header>...</header>
<Suspense fallback={<div>Loading everything...</div>}>
<Stats />
<Charts />
<RecentActivity />
</Suspense>
</div>
)
}
2. 优化加载状态
// 自定义加载组件
function StatsSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
)
}
export default function Page() {
return (
<div>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
</div>
)
}
3. 错误处理
// 错误边界
'use client'
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
export default function Page() {
return (
<div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent />
</Suspense>
</ErrorBoundary>
</div>
)
}
限制和注意事项
1. 实验性功能
// PPR 目前是实验性功能
// 可能在未来版本中发生变化
// 不建议在生产环境中使用
2. 兼容性
// 某些功能可能不兼容
// 需要测试和验证
// 关注官方更新
总结
Partial Prerendering (PPR) 的主要特点:
核心概念:
- 结合静态生成和动态渲染
- 静态部分预渲染
- 动态部分流式渲染
性能优势:
- 更快的首屏渲染
- 更好的用户体验
- 逐步加载内容
最佳实践:
- 合理使用 Suspense
- 优化加载状态
- 处理错误情况
注意事项:
- 实验性功能
- 兼容性限制
- 需要测试验证