实验功能

此功能目前处于实验阶段,可能会有所变更,不建议用于生产环境

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

当用户访问路由时:

  • 服务端发送包含静态内容的“外壳”,确保快速的初始加载
  • 该“外壳”为异步加载的动态内容留下了“孔”
  • 动态“孔”并行流式传输 ,减少了页面的整体加载时间

🎥 观看: 为什么选择 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 用作封装动态内容并启用流式传输的边界

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>
    </>
  )
}

流式传输

流式传输将路由拆分成多个块,并在块准备就绪后逐步将其流式传输到客户端。这允许用户在整个内容渲染完成之前立即看到页面的某些部分

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

为了减少网络开销,完整的响应(包括静态 HTML 和流式动态部分)将通过单个 HTTP 请求发送。这避免了额外的往返,并提高了初始加载和整体性能

启用部分预渲染

您可以通过添加 ppr 选项到你的 next.config.ts 文件中来启用 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

示例

动态 APIs

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

// app/user.tsx
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>This will be prerendered</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>This will be prerendered</h1>
      <Suspense fallback={<TableSkeleton />}>
        <Table searchParams={searchParams} />
      </Suspense>
    </section>
  )
}

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

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