什么是部分预渲染(Partial Prerendering - PPR)?

87 阅读3分钟

什么是部分预渲染(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>&copy; 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
  • 优化加载状态
  • 处理错误情况

注意事项

  • 实验性功能
  • 兼容性限制
  • 需要测试验证