Потоковый серверный рендеринг нового поколения: архитектура React Server Components (RSC), протокол сериализации Flight Protocol, потоковые границы <Suspense> и гибридная модель Partial Prerendering (PPR).
Классический серверный рендеринг (Traditional SSR) страдает от фундаментальной проблемы «блокирующего водопада»:
Streaming Suspense SSR и Partial Prerendering в Next.js 16 устраняют эти ограничения: статическая оболочка (Shell) отдается из глобального CDN-кэша мгновенно (менее 30 мс), а медленные динамические блоки подгружаются в уже открытое HTTP-соединение через потоковые чанки Flight Payload по мере готовности данных.
Конвейер потокового рендеринга разделяет дерево компонентов на статический остов (Static Shell) и асинхронные границы (Suspense Boundaries). Сервер сразу транслирует предварительно скомпилированный HTML со скелетонами, а параллельно выполняющиеся серверные функции отдают фрагменты дерева по протоколу Flight:
+-------------------------------------------------------------------------------+
| BROWSER HTTP/2 STREAM CONNECTION |
| |
| 1. Instant Static Shell (< 30 ms из CDN): |
| <html><head>...</head><body> |
| <nav>Header Nav</nav> |
| <div id="suspense-slot-1"><!-- Skeleton Loader Placeholder --></div> |
| </body> |
| |
| ~~~~~~~~~~~~~~~~~ [HTTP-соединение остается ОТКРЫТЫМ] ~~~~~~~~~~~~~~~~~~~~~ |
| |
| 2. Flight Payload Stream (Chunk #1 - Данные профиля через 120 ms): |
| <script>$RC("suspense-slot-1", "<div>Контент профиля пользователя</div>") |
| M1:{"id":"UserAvatar","chunks":["static/js/avatar.js"],"name":"UserAvatar"} |
| |
| 3. Flight Payload Stream (Chunk #2 - Тяжелая аналитика через 480 ms): |
| <div id="suspense-slot-2"><!-- Замена скелетона на реальный график --></div>|
| |
| 4. Завершение соединения: </body></html> (HTTP Stream Closed) |
+-------------------------------------------------------------------------------+Включение experimental.ppr: 'incremental' в next.config.ts и маркировка динамических страниц директивой export const experimental_ppr = true.
Изоляция всех синхронных RSC компонентов и лейаутов, которые могут быть предварительно собраны во время build-time.
Компоненты с прямыми обращениями к cookies(), headers() или некэшируемым запросам к БД оборачиваются в границы с явным UI fallback.
Движок React Server DOM генерирует потоковые чанки Flight Payload, передавая сериализованные свойства и ссылки на клиентские модули.
Инлайн-скрипт $RC (React Container) бесшовно заменяет fallback-скелетоны на готовый интерактивный DOM без сдвигов верстки (zero CLS).
Что физически передается в браузер при открытии страницы с включенным Partial Prerendering (PPR) в первые 30 мс?
Ниже приведена архитектура корпоративной панели мониторинга с PPR, где статическое меню и шапка отдаются мгновенно, а финансовая статистика и живая лента событий стримятся параллельно через независимые границы <Suspense>.
import { Suspense } from 'react';
import { cookies, headers } from 'next/headers';
import { MetricCardsSkeleton, LiveFeedSkeleton } from '@/components/dashboard/Skeletons';
import { MetricCards } from '@/components/dashboard/MetricCards';
import { LiveEventsFeed } from '@/components/dashboard/LiveEventsFeed';
// 1. Активация инкрементального PPR для маршрута
export const experimental_ppr = true;
// Статический компонент Shell (рендерится на этапе сборки и живет в CDN)
function DashboardHeader() {
return (
<header className="border-b border-border pb-6 mb-8 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-text-primary tracking-tight">
Аналитический шлюз Enterprise
</h1>
<p className="text-sm text-text-secondary mt-1">
Потоковый мониторинг транзакций и метрик в реальном времени
</p>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs font-medium">
Edge CDN Ready
</span>
</div>
</header>
);
}
// 2. Медленный динамический компонент метрик (задержка БД ~ 180 ms)
async function DynamicFinancialMetrics() {
// Динамический доступ к контексту текущего пользователя
const cookieStore = await cookies();
const orgId = cookieStore.get('enterprise_org_id')?.value || 'org_default';
// Имитация высоконагруженного запроса к хранилищу ClickHouse / PostgreSQL
const metrics = await fetch(`https://api.internal.enterprise/v1/metrics?org=${orgId}`, {
cache: 'no-store'
}).then(res => res.json());
return <MetricCards data={metrics} />;
}
// 3. Сверхтяжелый компонент ленты аудита (задержка внешнего API ~ 450 ms)
async function DynamicAuditStream() {
const reqHeaders = await headers();
const traceId = reqHeaders.get('x-trace-id') || 'untraced';
const auditEvents = await fetch('https://api.internal.enterprise/v1/audit/stream', {
headers: { 'X-Trace-Context': traceId },
next: { revalidate: 0 }
}).then(res => res.json());
return <LiveEventsFeed events={auditEvents} />;
}
export default function AnalyticsDashboardPage() {
return (
<div className="w-full max-w-7xl mx-auto p-6">
{/* Мгновенная статическая оболочка */}
<DashboardHeader />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Поток 1: Финансовые показатели (разрешится через 180 ms) */}
<div className="lg:col-span-2 space-y-6">
<Suspense fallback={<MetricCardsSkeleton />}>
<DynamicFinancialMetrics />
</Suspense>
</div>
{/* Поток 2: Живая лента безопасности (разрешится через 450 ms) */}
<div className="lg:col-span-1">
<Suspense fallback={<LiveFeedSkeleton />}>
<DynamicAuditStream />
</Suspense>
</div>
</div>
</div>
);
}Когда сервер разрешает промис в компоненте DynamicFinancialMetrics, в открытый сокет передаются строки протокола Flight:
M1:{"id":"./components/MetricBadge.tsx","name":"MetricBadge"}
J0:[["$","div",null,{"className":"metric-card","children":[
["$","$L1",null,{"value":"$142,500","label":"MRR"}],
["$","p",null,{"children":"Прирост +14.2%"}]
]}]]React Client DOM декодирует эту структуру, сопоставляет идентификаторы клиентских модулей ($L1) и выполняет гранулярный Subtree Reconciliation без сброса скролла и без потери состояния фокуса пользователя.
Если вызвать const token = (await cookies()).get('token') прямо в теле функции export default async function Page(), Next.js будет вынужден перевести всю страницу целиком в режим динамического SSR. Статическая оболочка перестанет кэшироваться, а TTFB деградирует до скорости самого медленного запроса. Всегда спускайте вызовы cookies() и headers() внутрь изолированных компонентов, обёрнутых в <Suspense>.
Указание границы <Suspense> без пропса fallback приводит к тому, что React поднимает ожидание до ближайшей родительской границы или вовсе блокирует рендеринг страницы до завершения промиса. Всегда задавайте легковесный скелетон, соответствующий габаритам будущего контента для предотвращения скачков макета (Cumulative Layout Shift).
Flight Protocol сериализует данные в JSON-подобный формат. Передача функций, инстансов классов с методами (например, new Map(), экземпляры PrismaClient или функции обратного вызова onClick) из серверного компонента в клиентский вызовет ошибку сериализации Cannot serialize functions or circular objects in Flight stream.
Почему при потоковом рендеринге (Streaming SSR) сервер обязан вернуть HTTP-статус (например, 200 OK) ДО начала передачи динамических чанков?