В традиционных архитектурах Single Page Application (SPA) и классическом Server-Side Rendering (Next.js Pages Router) существовала фундаментальная проблема: весь JavaScript-код компонентов, отрисованных на сервере, был обязан загружаться в браузер для последующей гидратации.
По мере масштабирования кодовой базы размер клиентского бандла экспоненциально растет. Браузер вынужден скачивать, парсить и компилировать мегабайты JavaScript даже для тех частей интерфейса, которые являются абсолютно статичными: навигационных панелей, футеров, разметки статей, а также тяжелых серверных библиотек (парсеры Markdown, синтаксические анализаторы, форматирование дат, математические библиотеки). Это приводит к деградации ключевых метрик Web Vitals — в первую очередь TTI и INP.
Второй проблемой являются сетевые водопады (Network Waterfalls): клиентский компонент монтируется, инициирует сетевой запрос за данными, ждет ответа от API, затем монтирует дочерний компонент, который запускает собственный запрос.
React Server Components (RSC) в архитектуре Next.js App Router устраняют эти ограничения:
В Next.js App Router среда выполнения разделена на два изолированных, но тесно взаимодействующих мира: Server Module Graph и Client Module Graph.
По умолчанию все компоненты являются Server Components. Вам не требуется указывать специальные директивы для их активации. Разделение графов происходит в точке объявления Network Boundary с помощью директивы 'use client'.
Исследуйте границу сериализации React Flight и влияние директивы 'use client' на бандл
Директива 'use client' размещена на самом нижнем интерактивном листе (кнопке). Родительские данные и тяжелый Markdown-парсер исполняются на сервере и не попадают в JS-бандл браузера.
Важно понимать: компонент, помеченный директивой 'use client', все равно рендерится на сервере во время начального запроса для генерации статичного HTML. Директива 'use client' означает не «рендерить только в браузере», а «включить данный модуль и все его дочерние импорты в клиентский JavaScript-бандл для последующей гидратации».
Для описания серверного дерева используется RSC Flight Payload. Вместо разметки клиентских компонентов в поток помещается Client Reference.
Пользователь запрашивает маршрут. Next.js инициирует серверный рендеринг дерева сегментов маршрута (Layouts, Pages).
Сервер исполняет асинхронные серверные компоненты, параллельно запрашивая данные из баз данных и внешних API.
React сериализует дерево в поток Flight: серверные элементы превращаются в структуру данных, а клиентские — в Client References плейсхолдеры с сериализованными пропсами.
Сервер мгновенно отдает начальный HTML для Fast First Paint, параллельно стримя чанки RSC Payload и Suspense-границы через HTTP-стриминг.
Браузер парсит HTML, загружает минимальные JS-чанки для компонентов с 'use client' и гидратирует интерактивные узлы без повторного рендеринга серверной структуры.
Означает ли указание директивы 'use client' в начале файла, что компонент будет компилироваться и рендериться исключительно в браузере?
Серверные компоненты объявляются как стандартные функции. Главное отличие — возможность делать компонент асинхронным (async) и напрямую выполнять await промисов.
В соответствии со стандартами Next.js 16 параметры маршрута (params и searchParams) типизируются как Promise и требуют обязательного await.
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function UserPage({ params, searchParams }: PageProps) {
// В Next.js 16 доступ к params и searchParams строго асинхронный
const { id } = await params;
const query = await searchParams;
const activeTab = typeof query.tab === 'string' ? query.tab : 'overview';
// Прямой запрос к базе данных без промежуточных fetch/REST API
const user = await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, role: true }
});
if (!user) {
notFound();
}
return (
<div className="p-8 max-w-2xl mx-auto">
<h1 className="text-2xl font-bold text-text-primary">{user.name}</h1>
<p className="text-text-muted">{user.email}</p>
<div className="mt-4 text-xs font-mono text-accent-cyan">
Активная вкладка: {activeTab}
</div>
</div>
);
}'use client')#Директива 'use client' объявляется первой строкой файла до любых импортов или комментариев.
Она требуется только тогда, когда компоненту необходимы:
useState, useReducer, useEffect, useLayoutEffect.onClick, onChange, onSubmit, onKeyDown.window, document, localStorage, navigator, WebSockets.'use client';
import { useState, useTransition } from 'react';
interface CounterProps {
initialCount?: number;
}
export function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
const [isPending, startTransition] = useTransition();
const handleIncrement = () => {
startTransition(() => {
setCount((prev) => prev + 1);
});
};
return (
<div className="flex items-center gap-4">
<button
type="button"
disabled={isPending}
onClick={handleIncrement}
className="px-4 py-2 rounded-lg bg-accent-violet hover:bg-accent-violet/90 text-white font-medium transition-colors disabled:opacity-50"
>
Инкремент: {count}
</button>
{isPending && <span className="text-xs text-text-muted">Обновление...</span>}
</div>
);
}'use client' против 'use server'#Одна из самых частых ошибок в сообществе — предположение, что директива 'use server' делает компонент серверным. Это не так:
'use client': Маркирует файл как границу клиентского бандла (Client Boundary).'use server': Объявляет Server Action — серверную функцию для мутации данных (RPC эндпоинт), вызываемую по HTTP POST. Она не создает серверный UI-компонент!Если интерактивному клиентскому компоненту (например, модальному окну, аккордеону или выпадающему меню) требуется отрендерить внутри себя тяжелый серверный компонент, нельзя импортировать серверный компонент внутрь клиентского.
Вместо этого используйте паттерн композиции через children или именованные JSX-слоты:
'use client';
import { useState } from 'react';
interface ModalDialogProps {
triggerLabel: string;
children: React.ReactNode; // Серверный компонент передается снаружи!
}
export function ModalDialog({ triggerLabel, children }: ModalDialogProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
type="button"
onClick={() => setIsOpen(true)}
className="px-3 py-1.5 rounded-md bg-bg-elevated border border-border text-sm"
>
{triggerLabel}
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-bg-surface border border-border rounded-xl p-6 max-w-lg w-full">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">Детали</h2>
<button type="button" onClick={() => setIsOpen(false)}>✕</button>
</div>
{/* Здесь рендерится серверный слот, оставаясь серверным кодом */}
{children}
</div>
</div>
)}
</>
);
}import { ModalDialog } from '@/components/ModalDialog';
import { HeavyAnalyticsChart } from '@/components/HeavyAnalyticsChart'; // Server Component с тяжелыми библиотеками
export default async function DashboardPage() {
return (
<main className="p-6">
<h1 className="text-xl font-bold mb-4">Аналитика</h1>
{/* Donut Pattern: передаем Server Component как children в Client Component */}
<ModalDialog triggerLabel="Открыть отчет">
<HeavyAnalyticsChart />
</ModalDialog>
</main>
);
}use())#В React 19 и Next.js 16 через границу сериализации пропсов можно передавать не только примитивы, но и неразрешенные объекты Promise. Это позволяет не блокировать TTFB серверного компонента и разворачивать промис внутри клиентского интерфейса через хук use().
import { Suspense } from 'react';
import { ItemComments } from './ItemComments';
import { fetchCommentsPromise } from '@/lib/data';
export default async function ItemPage({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Инициируем запрос БЕЗ await: промис начинает выполняться в фоне
const commentsPromise = fetchCommentsPromise(id);
return (
<div>
<h1>Карточка товара #{id}</h1>
{/* Оборачиваем в Suspense для отображения скелетона во время резолва промиса */}
<Suspense fallback={<p className="text-text-muted">Загрузка комментариев...</p>}>
<ItemComments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}'use client';
import { use } from 'react';
interface Comment {
id: string;
author: string;
text: string;
}
export function ItemComments({
commentsPromise
}: {
commentsPromise: Promise<Comment[]>;
}) {
// React 19 хук use() разворачивает промис напрямую внутри клиентского компонента
const comments = use(commentsPromise);
return (
<div className="space-y-3 mt-4">
<h3 className="font-semibold text-text-primary">Комментарии ({comments.length}):</h3>
{comments.map((comment) => (
<div key={comment.id} className="p-3 rounded-lg border border-border bg-bg-elevated/40">
<span className="font-medium text-accent-cyan text-sm">{comment.author}</span>
<p className="text-sm text-text-muted mt-1">{comment.text}</p>
</div>
))}
</div>
);
}Для предотвращения случайного импорта серверных утилит на клиент установите пакет server-only:
pnpm add server-onlyДля защиты чувствительных объектов (DTO из базы данных) используйте React 19 Taint API:
import 'server-only';
import { experimental_taintObjectReference } from 'react';
import { db } from '@/lib/db';
export async function getUserAccount(id: string) {
const user = await db.user.findUnique({
where: { id }
});
if (user) {
// Если кто-то попытается передать объект user целиком в Client Component,
// React немедленно выбросит ошибку при сборке или рендере
experimental_taintObjectReference(
'Запрещено передавать сырой объект пользователя в клиентский компонент во избежание утечки хэша пароля',
user
);
}
return user;
}Какой из следующих типов данных МОЖНО безопасно передать в качестве пропса из Server Component в Client Component через границу React Flight?
Многие разработчики по аналогии с 'use client' ошибочно пишут 'use server' в начале файла UI-компонента, пытаясь сделать его серверным.
В App Router все компоненты по умолчанию серверные. Директива 'use server' предназначена исключительно для объявления Server Actions (асинхронных RPC-эндпоинтов для мутации данных через POST-запросы). Размещение 'use server' в файле обычного UI-компонента приведет к ошибке компилятора Next.js.
Если вы запрашиваете запись пользователя из базы данных и передаете объект целиком в Client Component, все поля записи (включая passwordHash, stripeCustomerId, внутренние токены) будут сериализованы в открытый RSC Flight Payload, доступный любому пользователю во вкладке Network DevTools.
Решение: Всегда проецируйте данные в безопасный DTO с минимально необходимым набором полей.
// ❌ АНТИПАТТЕРН: Вся запись уходит в открытый RSC Flight Payload
const user = await db.user.findUnique({ where: { id } });
return <UserBadge user={user} />;
// ✅ ПРАВИЛЬНО: Сериализуются только публичные свойства
return <UserBadge name={user.name} avatarUrl={user.avatarUrl} />;Если вы попытаетесь напрямую импортировать файл серверного компонента в файл с директивой 'use client', сборщик Turbopack автоматически включит его в клиентский граф модулей. Это приведет к раздуванию бандла или ошибке сборки из-за невозможности выполнить серверные библиотеки (БД, fs, приватные переменные окружения) в браузере. Для вложенности используйте Donut Pattern (children или слоты).
Если в layout.tsx верхнего уровня выполнить медленный асинхронный запрос (const settings = await getSlowSettings()), браузер пользователя будет ждать ответа белого экрана, так как заблокируется отдача первого байта (TTFB) всего приложения.
Решение: Выносите медленные запросы данных глубже в дерево и изолируйте их с помощью <Suspense fallback={<Skeleton />}>.