跳到主要内容

部分预渲染

部分预渲染 (PPR) 是一种渲染策略,允许你在同一路由中结合静态和动态内容。这改善了初始页面性能,同时仍然支持个性化的动态数据。

提示

这项功能目前处于实验阶段,可能随时更改,不推荐用于生产环境。请尝试使用并在 GitHub 上分享您的反馈。

部分预渲染的产品页面,显示静态导航和产品信息,以及动态购物车和推荐产品部分预渲染的产品页面,显示静态导航和产品信息,以及动态购物车和推荐产品

当用户访问路由时:

  • 服务器发送包含静态内容的外壳,确保快速初始加载。
  • 外壳为将异步加载的动态内容留下空位
  • 动态空位并行流式传输,减少页面的总体加载时间。

🎥 观看: 为什么使用 PPR 以及它如何工作 → YouTube (10 分钟)

部分预渲染如何工作?

要理解部分预渲染,需要熟悉 Next.js 中可用的渲染策略。

静态渲染

使用静态渲染,HTML 会提前生成——要么在构建时,要么通过重新验证。结果被缓存并在用户和请求之间共享。

在部分预渲染中,Next.js 为路由预渲染一个静态外壳。这可以包括布局和任何其他不依赖请求时数据的组件。

动态渲染

使用动态渲染,HTML 在请求时生成。这允许你基于请求时数据提供个性化内容。

如果组件使用以下 API,它会变为动态:

在部分预渲染中,使用这些 API 会抛出一个特殊的 React 错误,通知 Next.js 组件无法静态渲染,导致构建错误。你可以使用 Suspense 边界包装组件以延迟渲染直到运行时。

Suspense

React Suspense 用于延迟渲染应用程序的部分内容,直到满足某些条件。

在部分预渲染中,Suspense 用于在组件树中标记动态边界

在构建时,Next.js 预渲染静态内容和 fallback UI。动态内容被推迟直到用户请求路由。

用 Suspense 包装组件不会使组件本身变为动态(你的 API 使用会使其变为动态),而是 Suspense 被用作封装动态内容并启用流式传输的边界。

app/page.js
import { Suspense } from 'react'
import StaticComponent from './StaticComponent'
import DynamicComponent from './DynamicComponent'
import Fallback from './Fallback'

export const experimental_ppr = true

export default function Page() {
return (
<>
<StaticComponent />
<Suspense fallback={<Fallback />}>
<DynamicComponent />
</Suspense>
</>
)
}

流式传输

流式传输将路由分成块,并在它们准备好时逐步流式传输给客户端。这允许用户在全部内容完成渲染之前立即看到页面的部分内容。

显示客户端上部分渲染页面的图表,带有正在流式传输的块的加载 UI。显示客户端上部分渲染页面的图表,带有正在流式传输的块的加载 UI。

在部分预渲染中,包装在 Suspense 中的动态组件从服务器并行开始流式传输。

显示流式传输期间路由段并行化的图表,显示各个块的数据获取、渲染和水合。显示流式传输期间路由段并行化的图表,显示各个块的数据获取、渲染和水合。

为了减少网络开销,完整响应——包括静态 HTML 和流式传输的动态部分——在单个 HTTP 请求中发送。这避免了额外的往返并改善了初始加载和整体性能。

启用部分预渲染

你可以通过在 next.config.ts 文件中添加 ppr 选项来启用 PPR:

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

const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental',
},
}

export default nextConfig

'incremental' 值允许你为特定路由采用 PPR:

/app/dashboard/layout.tsx
export const experimental_ppr = true

export default function Layout({ children }: { children: React.ReactNode }) {
// ...
}

没有 experimental_ppr 的路由将默认为 false,不会使用 PPR 进行预渲染。你需要为每个路由明确选择加入 PPR。

提示:

  • experimental_ppr 将应用于路由段的所有子级,包括嵌套布局和页面。你不需要将其添加到每个文件,只需要添加到路由的顶部段。
  • 要禁用子段的 PPR,你可以在子段中将 experimental_ppr 设置为 false

示例

动态 API

当使用需要查看传入请求的动态 API 时,Next.js 将为路由选择动态渲染。要继续使用 PPR,用 Suspense 包装组件。例如,<User /> 组件是动态的,因为它使用 cookies API:

app/user.js
import { cookies } from 'next/headers'

export async function User() {
const session = (await cookies()).get('session')?.value
return '...'
}

<User /> 组件将被流式传输,而 <Page /> 内的任何其他内容将被预渲染并成为静态外壳的一部分。

app/page.tsx
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'

export const experimental_ppr = true

export default function Page() {
return (
<section>
<h1>这将被预渲染</h1>
<Suspense fallback={<AvatarSkeleton />}>
<User />
</Suspense>
</section>
)
}

传递动态 props

只有当值被访问时,组件才会选择动态渲染。例如,如果你从 <Page /> 组件读取 searchParams,你可以将此值作为 prop 转发给另一个组件:

app/page.tsx
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'

export default function Page({
searchParams,
}: {
searchParams: Promise<{ sort: string }>
}) {
return (
<section>
<h1>这将被预渲染</h1>
<Suspense fallback={<TableSkeleton />}>
<Table searchParams={searchParams} />
</Suspense>
</section>
)
}

在表格组件内部,从 searchParams 访问值将使组件变为动态,而页面的其余部分将被预渲染。

app/table.tsx
export async function Table({
searchParams,
}: {
searchParams: Promise<{ sort: string }>
}) {
const sort = (await searchParams).sort === 'true'
return '...'
}