React Server Components: что реально меняют и когда выбирать

Фронтенд4 мин чтения
  • #react
  • #rsc
  • #server-components
  • #nextjs
  • #ssr

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 почти бесполезны. Боль — экосистема ещё не вся адаптирована и более сложная ментальная модель. Не серебряная пуля, а инструмент под конкретный класс приложений.

Что почитать