Антон Ларичев

Введение
Серверные компоненты React (RSC) стали одним из ключевых нововведений в экосистеме Next.js. В отличие от классического SSR, они выполняются исключительно на сервере и никогда не попадают в браузер. Это меняет фундаментальные принципы построения приложений — граница между сервером и клиентом становится явной частью архитектуры.
В этой статье разберём, как серверные компоненты устроены изнутри: что происходит на уровне React runtime, как данные передаются клиенту и почему этот подход быстрее традиционного SSR.
Серверные и клиентские компоненты: ключевые отличия
В Next.js App Router все компоненты по умолчанию являются серверными. Чтобы сделать компонент клиентским, нужно явно добавить директиву 'use client' в начало файла.
// Серверный компонент — выполняется только на сервере
async function UserProfile({ userId }: { userId: string }) {
// Прямой запрос к базе данных без API-слоя
const user = await db.users.findById(userId);
return (
<div>
<h2>{user.name}</h2>
<ClientSideWidget data={user.preferences} />
</div>
);
}
'use client';
// Клиентский компонент — работает и на сервере при гидратации, и в браузере
import { useState } from 'react';
function ClientSideWidget({ data }: { data: Preferences }) {
const [expanded, setExpanded] = useState(false);
return (
<button onClick={() => setExpanded(!expanded)}>
{expanded ? 'Скрыть' : 'Показать'}
</button>
);
}
Серверный компонент может импортировать клиентский, но не наоборот. Это однонаправленная граница.
RSC Payload: формат передачи данных
Когда Next.js рендерит страницу с серверными компонентами, он не отправляет только готовый HTML. Вместо этого создаётся специальный формат — RSC Payload.
RSC Payload — это бинарный поток данных, похожий на JSON, но расширенный. Он содержит:
- Дерево компонентов в сериализованном виде
- Пропсы и данные, полученные на сервере
- Ссылки на клиентские компоненты (module references)
- Данные для Suspense и стриминга
// Упрощённое представление RSC Payload
1:{"type":"div","props":{"className":"wrapper"},"children":["$2"]}
2:{"type":"$Lc/components/Button.js","props":{"label":"Нажми"},"children":null}
В реальности формат использует числовые идентификаторы для ссылок и передаётся потоком по мере выполнения компонентов.
Как Next.js обрабатывает запрос
Шаг 1: получение запроса
Next.js принимает HTTP-запрос и на основе файловой системы App Router определяет, какие layout и page нужно рендерить.
Шаг 2: выполнение серверных компонентов
React запускает рендеринг дерева компонентов. Каждый серверный компонент может быть async и выполнять запросы к БД, внешним API или файловой системе.
// Параллельные запросы данных — не блокируют друг друга
async function Dashboard() {
const [stats, notifications] = await Promise.all([
fetchStats(),
fetchNotifications(),
]);
return (
<main>
<StatsPanel data={stats} />
<NotificationList items={notifications} />
</main>
);
}
Шаг 3: стриминг через Suspense
Next.js поддерживает потоковую передачу HTML. Содержимое внутри <Suspense> отправляется клиенту по мере готовности, не блокируя отрисовку остальной страницы.
import { Suspense } from 'react';
async function Page() {
return (
<div>
<Header />
{/* Стримится отдельно, не задерживает Header */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent />
</Suspense>
</div>
);
}
Шаг 4: гидратация на клиенте
Браузер получает HTML и RSC Payload одновременно. React использует payload для восстановления дерева компонентов — гидратируются только клиентские компоненты, серверные повторно не рендерятся.
Кэширование и дедупликация запросов
fetch внутри серверных компонентов автоматически дедуплицируется в рамках одного запроса. Если несколько компонентов вызовут один и тот же URL, реальный HTTP-запрос выполнится только один раз.
// Оба компонента на одной странице сделают только один HTTP-запрос
async function ComponentA() {
const data = await fetch('https://api.example.com/config');
return <div>{/* использование data */}</div>;
}
async function ComponentB() {
// Результат берётся из кэша дедупликации
const data = await fetch('https://api.example.com/config');
return <div>{/* использование data */}</div>;
}
Частые ошибки
Использование браузерных API в серверных компонентах
Серверные компоненты выполняются в Node.js, где нет window, document или localStorage.
// Ошибка: window не существует в серверном окружении
function BadComponent() {
const width = window.innerWidth; // ReferenceError
return <div>{width}</div>;
}
// Правильно: браузерный API выносится в клиентский компонент
'use client';
function GoodComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>{width}</div>;
}
Передача несериализуемых пропсов
Пропсы из серверного компонента в клиентский должны сериализоваться в JSON. Функции и экземпляры классов передать нельзя.
// Ошибка: функция не сериализуется через RSC Payload
<ClientComponent onClick={() => console.log('click')} />
// Правильно: обработчик определяется внутри клиентского компонента
<ClientComponent itemId={item.id} />
Избыточное использование 'use client'
Добавление 'use client' везде по привычке из Pages Router лишает вас преимуществ RSC. Выносите в клиентские компоненты только то, что требует интерактивности или браузерных API. Всё остальное должно оставаться серверным.
Заключение
Серверные компоненты Next.js — это не просто SSR с другим синтаксисом. Это новая архитектурная модель, где граница сервер/клиент стала явной частью кода. RSC Payload, стриминг через Suspense и автоматическая дедупликация запросов позволяют строить быстрые приложения с минимальным JavaScript на клиенте.
Главные выводы:
- Серверные компоненты выполняются только на сервере и не увеличивают JS-бандл
- RSC Payload передаёт сериализованное дерево компонентов, а не только HTML
- Стриминг позволяет отображать части страницы по мере их готовности
- Пропсы между серверными и клиентскими компонентами обязаны быть сериализуемы в JSON






Комментарии
0