Next.js Partial Prerendering 完全指南:用 PPR 融合静态与动态渲染 (2026)

Next.js Partial Prerendering (PPR) 在同一页面同时输出静态预渲染外壳和流式动态片段。本文涵盖 PPR 工作原理、与 ISR/SSR/SSG 对比、Next.js 15 启用步骤、电商商品页实战示例和常见踩坑修复。

更新于:2026 年 5 月 28 日

Partial Prerendering(PPR,部分预渲染)是 Next.js 推出的一种新型渲染策略。简单说,它允许你在同一个页面里同时输出静态预渲染的「外壳」和动态流式渲染的内容:用户首屏看到 CDN 边缘的静态 HTML,而个性化部分则通过 React Suspense 边界异步流入。我从 pages-only 时代就开始写 Next.js,老实说在很长一段时间里,我都觉得 SSG、SSR、ISR 这三套渲染心智模型已经足够复杂了。但用过 PPR 之后我意识到,它真的解决了一个我们以前只能靠客户端骨架屏来掩饰的问题。

  • PPR 在构建时预渲染页面的静态外壳(shell),并在请求时通过 React Suspense 边界流式注入动态内容,单页面同时享有 SSG 的速度和 SSR 的个性化。
  • 截至 Next.js 15.3(2026 年),PPR 仍标记为实验性(experimental),需要在 next.config.ts 中显式启用 experimental.ppr
  • PPR 与 ISR 不同:ISR 整页重新生成有 TTL,PPR 每次请求都流式渲染动态片段,且无 TTL 概念。
  • 启用 PPR 必须使用 App Router,并将动态逻辑(cookies、headers、searchParams、未缓存 fetch)包裹在 <Suspense> 中,否则整页会回退到完全动态渲染。
  • PPR 显著降低 TTFB(首字节时间),尤其适合电商商品页、博客带评论数的文章页、Dashboard 这类「静态壳 + 动态片段」场景。

什么是 Next.js Partial Prerendering?

Partial Prerendering 是一种把构建时预渲染请求时动态渲染合并到同一个响应里的策略。它的输出可以这样理解:当用户访问 /product/123 时,Next.js 立即从 CDN 边缘返回一个已经预渲染好的 HTML 外壳(包含导航栏、商品标题、固定描述、SEO 标签),同时为页面里那些需要「实时数据」的区域(库存数量、个性化推荐、登录用户的购物车按钮)保留占位符。这个 HTML 流并不会立刻关闭,服务端会通过 React Suspense 流式渲染 把动态片段的内容追加进来,浏览器一边接收一边补全 DOM。

这个特性最早由 Vercel 在 Next.js Conf 2023 演示,2024 年作为实验性 API 进入 14.x 系列,2026 年初的 15.3 版本仍处于 experimental.ppr 标志位之下。从用户视角看,他们感受到的是静态站点的速度,加上动态站点的鲜度,这是过去 Next.js 没有任何单一渲染模式能完成的承诺。

我在迁移一个旧 pages-router 项目时,做的第一件事就是把首页改造成 PPR。原本 SSR 的 TTFB 大约 380ms,启用 PPR 后稳定在 70ms 以内,而那些个性化的「为你推荐」卡片只比之前晚 150ms 出现。但首屏 LCP 元素已经在屏幕上了,用户的感知是瞬间

PPR 的工作原理:静态外壳与动态洞

要理解 PPR,最好的方式是把页面想成一块「瑞士奶酪」:大部分是静态可预渲染的奶酪体,而每一个 <Suspense> 边界就是一个「洞」,洞里的内容在请求时才被填充。Next.js 在 next build 阶段会做这件事:

  1. 遍历组件树,识别每一个动态 API 调用(cookies()headers()searchParams、未缓存的 fetch())。
  2. 检查这些动态调用是否被包裹在某个 <Suspense fallback={...}> 内。
  3. 对 Suspense 之外的部分,进行静态预渲染并写入构建产物。
  4. 对 Suspense 之内的部分,标记为请求时流式渲染

请求到来时,CDN 立刻返回静态外壳(带占位符),与此同时 origin 服务器开始执行动态片段并把结果流式推送给浏览器。HTTP 响应使用 Transfer-Encoding: chunked,整个过程基于 React 18+ 的 renderToReadableStream 实现,浏览器原生支持。

这里的关键点很简单:任何未被 Suspense 包裹的动态 API 调用都会让整页退化成完全动态渲染。这是我和团队踩过最多的坑。在我们一个 Dashboard 项目中,仅仅因为在 layout 里调用了 headers() 来读取 User-Agent,整个路由的所有页面都没有静态外壳了。修复方法是把 headers() 推到一个被 Suspense 包裹的子组件里。

如何在 Next.js 15 中启用 PPR?

启用 PPR 只需要两步配置加一处页面声明。前提是你已经在使用 App Router(Pages Router 不支持 PPR)。

步骤 1:升级到 Next.js 15.x 与 React 19

npm install next@latest react@latest react-dom@latest
# 或
pnpm add next@latest react@latest react-dom@latest

步骤 2:在 next.config.ts 中启用实验性标志

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    // 'incremental' 表示按页面手动启用,推荐用于渐进式迁移
    // 设为 true 则全站启用(仅推荐新项目)
    ppr: 'incremental',
  },
}

export default nextConfig

步骤 3:在页面中声明 PPR

使用 incremental 模式时,每个页面需要显式导出 experimental_ppr = true

// app/product/[id]/page.tsx
import { Suspense } from 'react'
import { ProductDetails } from './product-details'
import { LiveInventory } from './live-inventory'
import { InventorySkeleton } from './inventory-skeleton'

// 启用 PPR
export const experimental_ppr = true

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <main>
      {/* 静态外壳:在构建时预渲染 */}
      <ProductDetails id={id} />

      {/* 动态洞:请求时流式注入 */}
      <Suspense fallback={<InventorySkeleton />}>
        <LiveInventory id={id} />
      </Suspense>
    </main>
  )
}

构建后你会在终端看到这样的输出,其中 标记表示该路由已成功启用 PPR:

Route (app)                         Size    First Load JS
◐ /product/[id]                     2.1 kB        102 kB
   └ ◐ (PPR)

PPR 与 ISR、SSR、SSG 有什么区别?

这是我被问得最多的一个问题。下面这张表把四种渲染模式的关键差异列清楚了,尤其注意「动态片段」和「重新验证」这两列,它们是 PPR 与 ISR 最容易混淆的地方。

维度 SSG ISR SSR PPR
渲染时机 构建时 构建 + 后台重新验证 每次请求 构建时(壳)+ 请求时(洞)
TTFB 极低(CDN) 极低(CDN) 较高(依赖 origin) 极低(CDN 返回壳)
个性化内容 不支持 不支持 支持整页 支持片段
重新验证 需重建 TTL 或按需 动态片段始终最新
Suspense 必需
典型场景 文档站 博客、营销页 Dashboard 电商、社交、混合页

简而言之,PPR ≠ ISR + Suspense。ISR 的本质是「整页缓存 + TTL 后台再生」,它不能给同一份缓存里的不同用户看不同内容。PPR 的本质是「静态壳永远复用 + 每次请求流式注入个性化片段」,它根本没有 TTL 的概念,动态部分每次都是最新的。如果你做的是博客或文档站,ISR 还是更经济的选择;如果你做的是电商或带用户态的页面,那 PPR 才是你想要的。

实战示例:电商商品页的 PPR 改造

下面是一个完整的可运行示例,演示如何把一个传统 SSR 商品页改造成 PPR。我在生产环境跑过这个模式,TTFB 从 ~350ms 降到 ~80ms。

// app/product/[slug]/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'

export const experimental_ppr = true

// 商品基础信息:可静态预渲染(在构建时通过 generateStaticParams 预生成)
async function ProductInfo({ slug }: { slug: string }) {
  // 这个 fetch 默认会被缓存
  const res = await fetch(`https://api.shop.com/products/${slug}`, {
    next: { revalidate: 3600 },
  })
  const product = await res.json()

  return (
    <header>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>¥{product.price}</span>
    </header>
  )
}

// 动态片段 1:库存(实时)
async function StockBadge({ slug }: { slug: string }) {
  const res = await fetch(`https://api.shop.com/stock/${slug}`, {
    cache: 'no-store', // 标记为动态
  })
  const { count } = await res.json()
  return <p>库存:{count} 件</p>
}

// 动态片段 2:基于登录态的推荐
async function PersonalRecs() {
  const cookieStore = await cookies()
  const userId = cookieStore.get('user_id')?.value
  if (!userId) return <GuestRecs />

  const res = await fetch(`https://api.shop.com/recs?user=${userId}`, {
    cache: 'no-store',
  })
  const items = await res.json()
  return (
    <ul>
      {items.map((it: { id: string; name: string }) => (
        <li key={it.id}>{it.name}</li>
      ))}
    </ul>
  )
}

function GuestRecs() {
  return <p>登录后查看个性化推荐。</p>
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  return (
    <article>
      {/* 静态壳:构建时已预渲染 */}
      <ProductInfo slug={slug} />

      {/* 洞 1:库存(毫秒级流式注入) */}
      <Suspense fallback={<p>加载库存…</p>}>
        <StockBadge slug={slug} />
      </Suspense>

      {/* 洞 2:个性化推荐 */}
      <Suspense fallback={<p>加载推荐…</p>}>
        <PersonalRecs />
      </Suspense>
    </article>
  )
}

export async function generateStaticParams() {
  const res = await fetch('https://api.shop.com/products/top')
  const top = await res.json()
  return top.map((p: { slug: string }) => ({ slug: p.slug }))
}

这个例子里值得注意的几点:generateStaticParams 让前 N 个热门商品在构建时预生成;cookies() 调用被严格限制在 PersonalRecs 组件内,且该组件被 Suspense 包裹,所以静态壳不受影响;cache: 'no-store' 是触发动态渲染的明确信号。把这个改成 Server Actions 驱动的「加入购物车」 也只需要在按钮上加一个 form action,不会破坏 PPR。

PPR 常见踩坑与调试

从我自己和团队成员的经验看,PPR 失效几乎都出在以下几类问题上,按出现频率排序。

1. 在 layout.tsx 顶层调用动态 API

如果你在 app/layout.tsxapp/(group)/layout.tsx 顶层调用了 cookies()headers(),那么这个布局下的所有页面都会变成完全动态渲染。修复方式:把动态逻辑抽到一个客户端组件,或者一个被 Suspense 包裹的服务端组件里。

2. 第三方库在模块顶层读取环境

有些分析或 i18n 库在导入时就调用 headers(),这会污染整个路由。我曾经被一个 sentry 中间件的初始化代码坑过半天。解决办法是把这类库的调用延迟到组件渲染期。

3. 忘了 fallback 或 fallback 不稳定

Suspense 的 fallback prop 必须返回稳定的、可在静态壳里预渲染的 JSX。不要在 fallback 里再调用动态 API,那会导致死循环式的渲染降级。

4. 与中间件冲突

如果你的 middleware.ts 改写了响应,或者注入了基于用户的 header,可能会让 CDN 无法缓存静态壳。我在 Next.js 中间件完全指南 里详细讨论过中间件与 PPR 的协同模式,简单原则是:中间件只做路由判定,不做内容改写。

什么时候应该使用 PPR?

不是所有场景都适合 PPR。下面是我在做技术评审时用的决策清单。

  • 适合:电商商品/分类页、博客文章页(评论数/点赞数实时)、SaaS 着陆页带个性化 CTA、社交媒体 profile 页、新闻聚合站、文档站带「我的收藏」按钮。
  • 不适合:纯静态文档站(直接 SSG 就够),整页都是个性化的 Dashboard(用 SSR 更直接),SPA 风格的应用控制台(客户端渲染配合 SWR 更轻量)。
  • 看情况:营销活动页。如果有 A/B 测试和地区差异化,PPR 比 ISR 更合适;纯落地页用 SSG 即可。

另一个值得考虑的维度是部署目标。PPR 在 Vercel 上零配置工作,因为 Vercel 的 Edge Network 原生理解 PPR 的混合产物。Vercel 的 PPR 工程博客 详细解释了平台层是怎么把静态壳放在边缘、把动态片段路由到 origin 的。如果你自托管,需要确保前置的 CDN(如 Cloudflare、Fastly)正确处理 Transfer-Encoding: chunked 的混合响应。大多数现代 CDN 都支持,但要小心 buffering 设置。

最后给一句来自老 Next.js 用户的建议:不要为了 PPR 而 PPR。先用 Next.js 自带的 next build 输出和 Lighthouse 报告分析现状,确认你确实有「静态壳 + 动态片段」的真实需求,再启用。盲目把所有页面打开 experimental_ppr,反而会因为某些页面的动态 API 没包裹好,导致大面积的渲染回退,这一点 Next.js 官方 PPR 文档 也在多处反复强调。

常见问题

Partial Prerendering 在 Next.js 15 中已经稳定了吗?

截至 Next.js 15.3(2026 年 5 月),PPR 仍是实验性特性,必须在 next.config.ts 中显式启用 experimental.ppr。API 名称带 experimental_ 前缀,未来稳定后会去掉。生产环境可用,但要在升级 Next.js 时仔细阅读 changelog。

PPR 和 React Server Components 是同一个东西吗?

不是。RSC 是组件模型(决定哪些组件在服务端运行),PPR 是渲染策略(决定哪部分在构建时输出、哪部分在请求时流式注入)。PPR 依赖 RSC + Suspense 才能实现,但 RSC 在没有 PPR 的项目里也能用。

启用 PPR 后我还需要使用 ISR 吗?

视场景而定。PPR 适合「静态壳 + 动态片段」的混合页面,ISR 适合「整页缓存、定期或按需重新生成」的内容站。两者可以共存于同一应用的不同路由,并不互斥。

PPR 能在 Pages Router 中使用吗?

不能。PPR 是 App Router 独占特性,它依赖 React Server Components 和 React 18+ 的流式渲染能力。如果你还在 Pages Router 上,需要先迁移到 App Router 才能使用 PPR。

自托管 Next.js 应用如何支持 PPR?

使用 next start 自托管时 PPR 默认工作。如果前置了 CDN(Cloudflare、Fastly、Nginx),需要确保它们透传 Transfer-Encoding: chunked 并禁用响应 buffering,否则用户拿不到流式片段,体验就等同于 SSR。

Ben Howard
关于作者 Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.