Для решения типовых задач фронтенд-разработчику часто хватает базовых правил: «компоненты по умолчанию серверные», а для интерактивности нужно добавить 'use client'. Однако для Lead-архитекторов и Senior-инженеров поверхностного понимания недостаточно.
Когда приложение масштабируется до сотен маршрутов и миллионов пользователей, неизбежно возникают сложные низкоуровневые вопросы:
Date, BigInt, Map, Set, неразрешённые Promise) свободно пересекают границу сети, а функции и классы вызывают фатальные ошибки сериализации?Глубокое понимание React Flight Protocol и внутреннего устройства сборщика Turbopack позволяет проектировать высокопроизводительные системы с минимальным оверхедом и молниеносно локализовать архитектурные сбои в продакшене.
Рендеринг и доставка React Server Components — это не однократная генерация HTML, а потоковый конвейер, связывающий два рантайма (Node.js/Edge и Browser).
При первоначальном входе на сайт сервер отдаёт HTML-каркас (для мгновенного отображения первого контента FCP) вместе с потоком Flight Payload.
Однако при последующих переходах пользователя по ссылкам <Link> браузер не перезагружает HTML-страницу. Вместо этого инициируется Soft Navigation (Мягкая навигация): браузер отправляет фоновый сетевой запрос с заголовком RSC: 1 (или параметром ?_rsc=...), получая чистый поток инструкций Flight.
КЛИЕНТ (Браузер) СЕРВЕР (Node.js/Edge)
│ │
(1)│── Клик по ссылке <Link href="/analytics"> ───────►│
│ GET /analytics?_rsc=123 (Заголовок RSC: 1) │
│ │
(2)│ │ 1. Исполнение Server Components
│ │ 2. Сериализация в Flight Protocol
│ │ - Resource Hints: H
│ │ - Client References: I
│ │ - UI Virtual Tree: J
│ │
(3)│◄── Поток RSC Flight Payload (Chunks) ─────────────┤
│ │
(4)│ 1. Парсинг потока Flight │
│ 2. Turbopack догружает нужные JS-чанки (I) │
│ 3. React Reconciler согласует виртуальный DOM │
│ 4. Обновление UI БЕЗ перезагрузки страницы! │
│ (Стейт активных Client Components сохранен) │Сервер запускает функции компонентов, разрешает асинхронные запросы к БД и генерирует виртуальное дерево элементов.
Дерево преобразуется в строковый поток. Серверные узлы превращаются в JSON-теги, а клиентские компоненты — в Client References дескрипторы.
Рантайм React инспектирует передаваемые пропсы. Если в поток попадает объект с меткой taint, сериализация прерывается ошибкой безопасности.
Чанки Flight передаются через HTTP-стрим. Браузер начинает обработку первых строк до завершения рендеринга всей страницы.
React на клиенте встраивает новое поддерево в живой DOM, сверяя ключи с Client Component Manifest, и активирует интерактивность.
Что запрашивает браузер у сервера, когда пользователь кликает по внутренней ссылке <Link href='/analytics'> в Next.js App Router?
В сетевом потоке Flight Payload представляет собой набор текстовых строк, разделенных символом перевода строки \n. Каждая строка начинается с префикса, определяющего тип инструкции:
H["/static/css/app.css","stylesheet"]
I["(app-pages-browser)/./components/Counter.tsx",["static/chunks/counter.js"],"Counter"]
J0:["$","div",null,{"className":"container","children":[
["$","h1",null,{"children":"Панель управления"}],
["$","$L1",null,{"initialCount":10}]
]}]H (Resource Hint): Resource Hint для немедленной фоновой загрузки стилей и шрифтов.I (Client Module Import): Запись Client Component Manifest, указывающая браузеру физический путь к JavaScript-чанку клиентского компонента.J (JSON Element Tree): Сериализованное дерево элементов. Символ $ обозначает React.createElement, а $L1 указывает на клиентский модуль из строки I.Анализируйте сырой бинарно-текстовый протокол сериализации React Server Components
Требует Flight stream вместо HTML
Передает клиентский снимок для диффа
Флаг предзагрузки через <Link>
1:I["./components/MetricCard.tsx",["chunks/metric_card_client.js"],"default"]0:{"name":"AnalyticsPage","children":["$","div",null,{"className":"p-6","children":["$","$L1",null,{"metric":"visitors"}]}]}2:L1:{"value":14280,"status":"fresh","timestamp":1741859200}Сервер не отправляет исходный код компонента MetricCard. Вместо этого он передает указатель на бандл-чанк, который клиентский браузер должен загрузить и гидратировать.
Остальные 90% кода страницы остаются на сервере и не попадают в JS-бандл (Zero-Bundle Size).
React Flight нативно поддерживает Date и BigInt. Никогда не используйте деструктивный хак JSON.parse(JSON.stringify())! Проецируйте выборку через явный типизированный DTO:
export interface OrderDTO {
id: string;
totalAmount: bigint; // Flight нативно сериализует BigInt!
createdAt: Date; // Flight нативно сериализует Date!
metadata: Map<string, string>; // Flight нативно сериализует Map!
}
export function toOrderDTO(raw: {
id: string;
total: bigint;
created_at: Date;
meta: Record<string, string>;
}): OrderDTO {
return {
id: raw.id,
totalAmount: raw.total,
createdAt: raw.created_at,
metadata: new Map(Object.entries(raw.meta)),
};
}import { db } from '@/lib/db';
import { toOrderDTO } from '@/lib/dto/order';
import { ClientOrderView } from './ClientOrderView';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function OrderPage({ params }: PageProps) {
const { id } = await params;
const rawOrder = await db.order.findUniqueOrThrow({ where: { id } });
// Преобразуем в безопасный DTO с нативными типами
const orderDTO = toOrderDTO(rawOrder);
// Передаем клиенту: Date, BigInt и Map сохранятся без потерь!
return <ClientOrderView order={orderDTO} />;
}Когда функция с директивой 'use server' передается в клиентский компонент (например, через .bind()), рантайм React шифрует привязанные аргументы с помощью Action Closure Encryption:
import { deleteUserAction } from '@/app/actions/user';
import { DeleteUserButton } from './DeleteUserButton';
export default async function UsersPage() {
const userId = 'usr_98741';
// .bind() шифрует аргументы в закрытый криптографический токен внутри Flight Payload!
// Клиент видит лишь непрозрачную RPC-ссылку вида: $F1 = ["$ACTION_ID_123", ["usr_98741"]]
const boundDelete = deleteUserAction.bind(null, userId);
return <DeleteUserButton onDelete={boundDelete} />;
}Почему вызов JSON.parse(JSON.stringify(data)) перед передачей данных из Server Component в Client Component считается вредным антипаттерном в современной разработке?
Попытка «очистить» объекты перед передачей клиенту через JSON.parse(JSON.stringify(data)) является грубой ошибкой:
Date превращаются в строки ISO 8601, требуя повторного new Date() на клиенте.BigInt выбрасывают фатальный TypeError: Do not know how to serialize a BigInt.Map и Set превращаются в пустые объекты {}.React Flight нативно поддерживает Date, BigInt, Map, Set и ArrayBuffer!
Решение: Проецируйте выборку через типизированные DTO без использования JSON.stringify.
Когда вы привязываете аргументы к Server Action для передачи в Client Component, Next.js зашифрует все привязанные аргументы и передаст их клиенту внутри строки W протокола Flight. Если замкнуть массив на 10 000 элементов или секретный ключ, бандл Flight раздуется на мегабайты, а чувствительные данные будут отправлены на устройство пользователя.
Решение: Передавайте в .bind() только публичные скалярные идентификаторы (ID сущности), а секретные ключи извлекайте на сервере непосредственно внутри тела Server Action.
// ❌ ОПАСНО: передача приватного ключа в bound action
// const boundAction = deleteUser.bind(null, userId, secretApiKey);
// ✅ ПРАВИЛЬНО: передавать только публичный ID
// const boundAction = deleteUser.bind(null, userId);Пользователь открыл сайт на версии деплоя v1. Вы выкатили v2, где хэши JS-чанков изменились. Пользователь кликает по ссылке:
?_rsc=... и получает Flight Payload от v2.I с хэшем нового чанка chunk-v2-abc.js.Как Next.js решает это под капотом: Next.js перехватывает сбой согласования версий и автоматически инициирует Hard Navigation (полную перезагрузку документа), незаметно обновляя вкладку пользователя до версии v2.
Объекты, десериализованные из Flight Payload, замораживаются React (Object.freeze). Попытка напрямую изменить свойство props.user.name = 'New' выбросит ошибку в strict mode или приведет к неконсистентному состоянию Reconciler при повторном визите на страницу.