Как использовать частичный предварительный рендеринг (Partial Prerendering)

Частичный предварительный рендеринг (Partial Prerendering, PPR) — это стратегия рендеринга, позволяющая сочетать статический и динамический контент на одном маршруте. Это улучшает начальную производительность страницы, сохраняя при этом поддержку персонализированных динамических данных.

Частично предварительно отрендеренная страница продукта с статичной навигацией и информацией о продукте, а также динамичной корзиной и рекомендуемыми товарами

Когда пользователь посещает маршрут:

  • Сервер отправляет оболочку (shell), содержащую статичный контент, обеспечивая быструю начальную загрузку.
  • В оболочке остаются пропуски (holes) для динамического контента, который загружается асинхронно.
  • Динамические пропуски загружаются параллельно (streamed in parallel), сокращая общее время загрузки страницы.

🎥 Видео: Зачем нужен PPR и как он работает → YouTube (10 минут).

Как работает частичный предварительный рендеринг?

Чтобы понять PPR, полезно ознакомиться со стратегиями рендеринга, доступными в Next.js.

Статический рендеринг

При статическом рендеринге HTML генерируется заранее — либо во время сборки, либо через ревалидацию. Результат кэшируется и используется для всех пользователей и запросов.

В PPR Next.js предварительно рендерит статическую оболочку для маршрута. Она может включать макет и любые другие компоненты, не зависящие от данных во время запроса.

Динамический рендеринг

При динамическом рендеринге HTML генерируется во время запроса. Это позволяет обслуживать персонализированный контент на основе данных запроса.

Компонент становится динамическим, если использует следующие API:

В PPR использование этих API вызывает специальную ошибку React, которая сообщает Next.js, что компонент не может быть статически отрендерен, что приводит к ошибке сборки. Вы можете использовать границу Suspense для оборачивания компонента, чтобы отложить рендеринг до времени выполнения.

Suspense

React Suspense используется для отложенного рендеринга частей приложения до выполнения определенных условий.

В PPR Suspense используется для обозначения динамических границ в дереве компонентов.

Во время сборки Next.js предварительно рендерит статичный контент и UI fallback. Динамический контент откладывается до момента запроса маршрута пользователем.

Обертывание компонента в 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>
    </>
  )
}

Потоковая передача (Streaming)

Потоковая передача разбивает маршрут на части и постепенно передает их клиенту по мере готовности. Это позволяет пользователю видеть части страницы сразу, до завершения рендеринга всего контента.

Диаграмма, показывающая частично отрендеренную страницу на клиенте с загрузочным UI для частей, передаваемых потоком.

В PPR динамические компоненты, обернутые в Suspense, начинают потоковую передачу с сервера параллельно.

Диаграмма, показывающая параллелизацию сегментов маршрута во время потоковой передачи, отображающая получение данных, рендеринг и гидратацию отдельных частей.

Чтобы уменьшить нагрузку на сеть, полный ответ — включая статичный HTML и потоковые динамические части — отправляется в одном HTTP-запросе. Это избегает дополнительных циклов обмена данными и улучшает как начальную загрузку, так и общую производительность.

Включение частичного предварительного рендеринга

Вы можете включить PPR, добавив опцию ppr в файл next.config.ts:

import type { NextConfig } from 'next'

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

export default nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: 'incremental',
  },
}

Значение 'incremental' позволяет применять PPR для конкретных маршрутов:

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

export default function Layout({ children }: { children: React.ReactNode }) {
  // ...
}
/app/dashboard/layout.js
export const experimental_ppr = true

export default function Layout({ children }) {
  // ...
}

Маршруты без experimental_ppr по умолчанию будут иметь значение false и не будут предварительно рендериться с использованием PPR. Вам нужно явно включать PPR для каждого маршрута.

Полезно знать:

  • experimental_ppr применяется ко всем дочерним элементам сегмента маршрута, включая вложенные макеты и страницы. Вам не нужно добавлять его в каждый файл, только в верхний сегмент маршрута.
  • Чтобы отключить PPR для дочерних сегментов, вы можете установить experimental_ppr в false в дочернем сегменте.

Примеры

Динамические API

При использовании динамических API, требующих анализа входящего запроса, Next.js переключается на динамический рендеринг для маршрута. Чтобы продолжить использование PPR, оберните компонент в Suspense. Например, компонент <User /> является динамическим, потому что использует API cookies:

import { cookies } from 'next/headers'

export async function User() {
  const session = (await cookies()).get('session')?.value
  return '...'
}
import { cookies } from 'next/headers'

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

Компонент <User /> будет передаваться потоком, в то время как остальной контент внутри <Page /> будет предварительно отрендерен и станет частью статической оболочки.

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

Передача динамических пропсов

Компоненты становятся динамическими только при доступе к значению. Например, если вы читаете searchParams из компонента <Page />, вы можете передать это значение другому компоненту как пропс:

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>
  )
}
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'

export default function Page({ searchParams }) {
  return (
    <section>
      <h1>Это будет предварительно отрендерено</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 '...'
}
export async function Table({ searchParams }) {
  const sort = (await searchParams).sort === 'true'
  return '...'
}