React Server Components: что реально меняют и когда выбирать
React Server Components (RSC) — самая обсуждаемая и наименее понятая фича React с 2023 года. Вокруг неё много тумана из-за путаницы с SSR и из-за того, что фича меняет саму модель того, «где живёт код». Разберём чётко: что это, чем отличается от привычного SSR, и в каких проектах окупается.
Главное: RSC — это не SSR
Server-Side Rendering (SSR) работает так: при HTTP-запросе сервер один раз рендерит всё дерево компонентов в HTML и отдаёт браузеру. Потом на клиенте React «оживляет» (hydrates) этот HTML, прикручивая обработчики событий. После гидратации всё работает как обычный CSR (client-side rendering) — состояние, эффекты, интерактивность.
RSC устроены иначе. Серверный компонент не существует в JS-бандле браузера вообще. Сервер рендерит его в особый формат (не HTML, а специальный сериализованный поток), передаёт клиенту, и React собирает дерево, в котором часть узлов — уже готовые «слепки» с сервера, а часть — обычные клиентские компоненты, которые гидратируются. Серверный компонент не «оживает» на клиенте никогда — он остаётся слепком.
Отсюда свойства:
- Серверный компонент выполняется только на сервере. Его код (
import'ы, логика) не попадает в JS-бандл, который скачивает браузер. - Серверный компонент может напрямую ходить в БД, читать файлы, использовать секреты — потому что он живёт в серверной среде.
- Серверный компонент не может использовать
useState,useEffect, обработчики событий (onClick) — потому что он не гидратируется и не реагирует на действия пользователя.
Граница серверного и клиентского
Клиентский компонент помечается директивой "use client" в начале файла. Это указывает сборщику: «этот модуль и его импорты должны попасть в клиентский бандл». Без этой директивы по умолчанию (в Next.js App Router) все компоненты — серверные.
// Серверный компонент (по умолчанию): ходит в БД, не имеет состояния
import { db } from '@/lib/db';
export default async function ProductList() {
const products = await db.product.findMany(); // прямо из БД, без fetch
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.name}
<AddToCartButton productId={p.id} /> {/* клиентский компонент */}
</li>
))}
</ul>
);
}
// Клиентский компонент — отдельно, с "use client"
'use client';
import { useState } from 'react';
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? 'В корзине' : 'Добавить'}
</button>
);
}
Граница задаётся кодом, и её легко нарушить. Попытка передать функцию из серверного компонента в клиентский упадёт — функции не сериализуются. Передать Date-объект в пропсах клиентского компонента — упадёт (нужно ISO-строкой). Серверный компонент можно импортировать в клиентский только как children, переданные сверху — не напрямую.
Что RSC реально дают
Меньше JS в бэкенде-богатых интерфейсах. Компоненты, которые только читают данные и рисуют разметку, не тащат за собой ни код, ни зависимости в браузер. Тяжёлая библиотека для форматирования дат или парсинга markdown остаётся на сервере — клиент её не скачивает.
Прямой доступ к серверным ресурсам. Не нужен отдельный API-эндпоинт, чтобы отдать данные из БД в компонент. Серверный компонент делает await db.query(...) прямо. Меньше бойлерплейта, меньше ручной сериализации.
Лучшая история с fetching данных. Несколько серверных компонентов могут параллельно запрашивать данные, не блокируя друг друга; сервер ближе к БД, поэтому запросы быстрее, чем если бы их делал браузер.
Где боль
Экосистема. Не все библиотеки адаптированы. Те, что полагаются на useState/useEffect на верхнем уровне модуля или в рантайме проверяют typeof window, в серверных компонентах ломаются. Большинство популярных UI-библиотек (Radix, shadcn/ui) уже работают, но нишевые — нет.
Ментальная модель. Надо постоянно держать в голове: «это где исполняется?». Легко случайно положить useState в серверный компонент и получить ошибку, или попытаться использовать клиентский хук в серверном. Это снижается с привычкой, но порог входа выше, чем у классического React.
Отладка. Ошибки на границе сервер/клиент (например, попытка передать несериализуемое значение) иногда дают невнятные сообщения. Инструментарий развивается, но пока сложнее, чем «всё в браузере».
Когда выбирать
RSC окупаются, когда:
- Контент-сайты и админки поверх БД. Много статичного отображения данных, мало интерактива. Серверные компоненты убирают API-слой и уменьшают бандл.
- Приложения, где важен размер клиентского JS. Медленный мобильный интернет, дешёвые устройства — каждый килобайт на счету.
- Уже Next.js App Router. RSC там включены по умолчанию, не пользоваться ими — значит воевать с фреймворком.
RSC мало что дают, когда:
- Богатые SPA. Редакторы (Figma-подобные, IDE), интерактивные дашборды с локальным состоянием, игры — почти всё дерево клиентское, серверных компонентов мало. Классический CSR (через Vite, CRA-наследников) по-прежнему норма.
- Команда только осваивает React. RSC добавляют концептуальной сложности; имеет смысл сначала уверенно владеть классической моделью.
Короткое summary
React Server Components — это про то, что часть дерева живёт только на сервере и не попадает в браузер ни кодом, ни бандлом. Это не замена SSR (SSR делает один HTML-слепок, RSC — постоянный обмен частями дерева). Серверные компоненты не имеют состояния и событий, зато могут ходить в БД и файлы напрямую. Окупаются в контент-ориентированных проектах и админках; для интерактивных SPA почти бесполезны. Боль — экосистема ещё не вся адаптирована и более сложная ментальная модель. Не серебряная пуля, а инструмент под конкретный класс приложений.
Что почитать
- React Docs: Server Components — раздел про
"use client"и модель серверных/клиентских компонентов. - Next.js App Router Fundamentals — как RSC работают в Next.js на практике.
- Dan Abramov: «The two Reacts» — концептуальный разбор модели «React на сервере и на клиенте».