БлогУслугиКарьера
Обсудить проект
БлогУслугиКарьераОбсудить проект
React / Next.js

Server Components vs Client Components: когда что использовать

Разбираемся в React Server Components и Client Components: принципы работы, правила выбора, паттерны композиции. Чек-лист для разработчиков.

Редакция Feature
Редакция Feature·
24 мар
·
12 мин
·
Server Components vs Client Components: когда что использовать

Зачем нужно разделение на серверные и клиентские компоненты

С появлением React Server Components (RSC) в экосистеме React произошёл фундаментальный сдвиг. Раньше всё было просто: компоненты рендерились на клиенте, а SSR был лишь оптимизацией для первоначальной загрузки. Теперь компоненты разделены на два типа, и понимание различий между ними — обязательный навык для любого React-разработчика.

Server Components выполняются исключительно на сервере. Их код никогда не попадает в клиентский бандл. Client Components работают в браузере и содержат интерактивную логику. Правильный выбор между ними напрямую влияет на производительность, размер бандла и пользовательский опыт.

В этой статье мы дадим чёткие критерии выбора, разберём типичные паттерны композиции и покажем ошибки, которые допускают даже опытные разработчики. Если вы работаете с Next.js 15, рекомендуем также прочитать нашу статью Next.js 16: что нового и как использовать в production.

Что такое Server Components

Принцип работы

Server Components рендерятся на сервере и отправляют клиенту готовый HTML вместе со специальным форматом данных (RSC Payload). JavaScript-код этих компонентов не включается в клиентский бандл. Это их ключевое преимущество.

В Next.js 15 (App Router) все компоненты по умолчанию являются серверными. Вам не нужно ничего делать, чтобы создать Server Component — просто напишите обычный компонент:

// app/products/page.tsx — это Server Component по умолчанию
import { db } from "@/lib/database";

export default async function ProductsPage() {
  // Прямой доступ к базе данных — без API-эндпоинтов
  const products = await db.product.findMany({
    orderBy: { createdAt: "desc" },
    take: 20,
  });

  return (
    <main>
      <h1>Каталог товаров</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.description}</p>
            <span>{product.price} ₽</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

Что можно делать в Server Components

  • Напрямую обращаться к базе данных (Prisma, Drizzle, SQL)
  • Читать файловую систему сервера
  • Использовать серверные API и приватные ключи
  • Вызывать async/await прямо в компоненте
  • Импортировать тяжёлые библиотеки без влияния на размер бандла
  • Рендерить Markdown, подсветку синтаксиса и другие тяжёлые операции

Чего нельзя делать в Server Components

  • Использовать useState, useEffect, useRef и другие хуки состояния
  • Назначать обработчики событий (onClick, onChange, onSubmit)
  • Использовать браузерные API (window, document, localStorage)
  • Использовать контекст React (useContext)
  • Импортировать CSS-in-JS библиотеки, зависящие от рантайма

Что такое Client Components

Принцип работы

Client Components — это привычные React-компоненты, которые выполняются в браузере. Чтобы объявить компонент клиентским, добавьте директиву 'use client' в начало файла:

"use client";

import { useState, useCallback } from "react";

export function ProductFilter({ categories }: { categories: string[] }) {
  const [selected, setSelected] = useState<string[]>([]);

  const toggleCategory = useCallback((category: string) => {
    setSelected((prev) =>
      prev.includes(category)
        ? prev.filter((c) => c !== category)
        : [...prev, category]
    );
  }, []);

  return (
    <div className="flex gap-2 flex-wrap">
      {categories.map((category) => (
        <button
          key={category}
          onClick={() => toggleCategory(category)}
          className={`px-3 py-1 rounded ${
            selected.includes(category)
              ? "bg-blue-600 text-white"
              : "bg-gray-200"
          }`}
        >
          {category}
        </button>
      ))}
    </div>
  );
}

Важное уточнение

Директива 'use client' не означает, что компонент рендерится только на клиенте. Client Components по-прежнему проходят SSR на сервере для генерации начального HTML. Директива указывает на границу — всё, что импортируется в файл с 'use client', становится частью клиентского бандла и гидрируется в браузере.

Когда нужны Client Components

  • Интерактивные элементы: кнопки, формы, слайдеры, модальные окна
  • Компоненты с состоянием: фильтры, корзина, счётчики
  • Компоненты с эффектами: анимации, таймеры, подписки на WebSocket
  • Компоненты, использующие браузерные API: геолокация, камера, clipboard

Чек-лист для принятия решений

Вот текстовая схема принятия решений, которую мы используем в наших проектах React-разработки:

Компонент нужно создать
│
├─ Есть ли интерактивность (onClick, onChange)?
│  ├─ Да → Client Component ('use client')
│  └─ Нет ↓
│
├─ Используется ли useState / useEffect / useRef?
│  ├─ Да → Client Component ('use client')
│  └─ Нет ↓
│
├─ Нужен ли доступ к браузерным API?
│  ├─ Да → Client Component ('use client')
│  └─ Нет ↓
│
├─ Нужен ли прямой доступ к БД / файловой системе?
│  ├─ Да → Server Component (по умолчанию)
│  └─ Нет ↓
│
├─ Использует ли тяжёлые зависимости для рендера?
│  ├─ Да → Server Component (не попадёт в бандл)
│  └─ Нет ↓
│
└─ По умолчанию → Server Component

Правило большого пальца: если компоненту не нужна интерактивность, оставляйте его серверным. Сомневаетесь — оставляйте серверным. Добавлять 'use client' всегда можно позже, когда появится необходимость.

Паттерны композиции

Паттерн 1: Передача серверных данных в клиентский компонент

Самый частый паттерн — получить данные в серверном компоненте и передать их как пропсы в клиентский:

// app/catalog/page.tsx — Server Component
import { db } from "@/lib/database";
import { ProductGrid } from "./product-grid"; // Client Component

export default async function CatalogPage() {
  const products = await db.product.findMany();
  const categories = await db.category.findMany();

  return (
    <main>
      <h1>Каталог</h1>
      {/* Серверные данные передаются как пропсы */}
      <ProductGrid products={products} categories={categories} />
    </main>
  );
}
// app/catalog/product-grid.tsx — Client Component
"use client";

import { useState } from "react";

export function ProductGrid({ products, categories }) {
  const [filter, setFilter] = useState("all");

  const filtered =
    filter === "all" ? products : products.filter((p) => p.category === filter);

  return (
    <div>
      <div className="flex gap-2 mb-4">
        <button onClick={() => setFilter("all")}>Все</button>
        {categories.map((cat) => (
          <button key={cat.id} onClick={() => setFilter(cat.slug)}>
            {cat.name}
          </button>
        ))}
      </div>
      <div className="grid grid-cols-3 gap-4">
        {filtered.map((product) => (
          <div key={product.id}>{product.name}</div>
        ))}
      </div>
    </div>
  );
}

Паттерн 2: Серверный компонент как children клиентского

Это мощный паттерн, позволяющий вставлять серверный контент внутрь клиентской обёртки:

// app/layout.tsx — Server Component
import { Sidebar } from "./sidebar"; // Client Component
import { Navigation } from "./navigation"; // Server Component

export default function Layout({ children }) {
  return (
    <Sidebar>
      {/* Navigation — серверный, но передаётся как children */}
      <Navigation />
      {children}
    </Sidebar>
  );
}
// app/sidebar.tsx — Client Component
"use client";

import { useState } from "react";

export function Sidebar({ children }) {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <div className="flex">
      <aside className={isOpen ? "w-64" : "w-16"}>
        <button onClick={() => setIsOpen(!isOpen)}>
          {isOpen ? "Свернуть" : "Развернуть"}
        </button>
      </aside>
      <main className="flex-1">{children}</main>
    </div>
  );
}

Ключевой момент: Navigation рендерится на сервере, несмотря на то что передаётся внутрь клиентского Sidebar. Это работает потому, что children — это уже готовый результат рендера, а не компонент, который нужно исполнить.

Нужна React-разработка?

Используем лучшие практики Server Components в ваших проектах

Заказать React-разработку

Паттерн 3: Разделение интерактивности

Если в компоненте только малая часть интерактивна, выделите её в отдельный клиентский компонент:

// app/article/[slug]/page.tsx — Server Component
import { getArticle } from "@/lib/articles";
import { LikeButton } from "./like-button"; // только кнопка — клиентская
import { ShareButton } from "./share-button"; // только кнопка — клиентская

export default async function ArticlePage({ params }) {
  const { slug } = await params;
  const article = await getArticle(slug);

  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.html }} />

      {/* Только интерактивные элементы — клиентские */}
      <div className="flex gap-4 mt-8">
        <LikeButton articleId={article.id} initialCount={article.likes} />
        <ShareButton url={`/article/${slug}`} title={article.title} />
      </div>
    </article>
  );
}

Весь контент статьи рендерится на сервере, а в клиентский бандл попадают только маленькие интерактивные кнопки.

Типичные ошибки

Ошибка 1: Избыточное использование 'use client'

Самая частая ошибка — ставить 'use client' на каждый компонент «на всякий случай». Это сводит на нет все преимущества Server Components.

// ПЛОХО: не нужна директива 'use client'
"use client"; // зачем? здесь нет интерактивности

export function Footer() {
  return (
    <footer>
      <p>2026 Company Inc.</p>
      <nav>
        <a href="/about">О нас</a>
        <a href="/contacts">Контакты</a>
      </nav>
    </footer>
  );
}
// ХОРОШО: Server Component по умолчанию
export function Footer() {
  return (
    <footer>
      <p>2026 Company Inc.</p>
      <nav>
        <a href="/about">О нас</a>
        <a href="/contacts">Контакты</a>
      </nav>
    </footer>
  );
}

Ошибка 2: Импорт серверного кода в клиентский компонент

При импорте модуля в файл с 'use client', этот модуль попадает в клиентский бандл. Если он использует серверные API ( fs, database), приложение сломается.

// ПЛОХО: db не может работать в браузере
"use client";
import { db } from "@/lib/database"; // ОШИБКА!

export function UserProfile() {
  // ...
}

Решение — загружайте данные в серверном компоненте-родителе и передавайте через пропсы.

Ошибка 3: Создание огромных клиентских границ

Если обернуть 'use client' вокруг корневого layout, все дочерние компоненты станут клиентскими:

// ПЛОХО: весь layout и его дети — клиентские
"use client";

export default function RootLayout({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// ХОРОШО: выделите провайдер в отдельный клиентский компонент
// providers.tsx
"use client";

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// layout.tsx — остаётся серверным
import { ThemeProvider } from "./providers";

export default function RootLayout({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}

Ошибка 4: Передача несериализуемых пропсов

Server Components передают данные клиентским через сериализацию. Нельзя передавать функции, Date-объекты, Map, Set, классы:

// ПЛОХО: функция не сериализуется
<ClientComponent onClick={() => console.log('click')}/>

// ПЛОХО: Date не сериализуется
<ClientComponent date={new Date()}/>

// ХОРОШО: примитивы и простые объекты
<ClientComponent dateISO={new Date().toISOString()}/>

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Влияние на производительность

Размер бандла

Правильное использование Server Components может драматически сократить размер клиентского JavaScript. Вот реальные цифры из нашего проекта:

Подход JS размер (gzip) LCP TTI
Всё клиентское (Pages Router) 287 KB 2.8 сек 4.1 сек
Смешанный (плохо разделённый) 198 KB 2.1 сек 3.2 сек
Оптимальный (SC + минимум CC) 89 KB 1.2 сек 1.8 сек

Разница в три раза по размеру бандла и вдвое по времени до интерактивности. О том, как добиться максимальных показателей PageSpeed, читайте в статье Оптимизация Next.js: от 50 до 100 баллов PageSpeed.

Streaming и Suspense

Server Components естественно интегрируются со Streaming SSR. Каждый серверный компонент, обёрнутый в Suspense, может стримиться независимо:

export default function DashboardPage() {
  return (
    <div>
      {/* Эти блоки загружаются параллельно и стримятся по мере готовности */}
      <Suspense fallback={<Skeleton />}>
        <SlowDataWidget />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <AnotherSlowWidget />
      </Suspense>
    </div>
  );
}

Пользователь видит контент поблочно, по мере его готовности на сервере, без необходимости ждать самый медленный запрос.

Стратегии тестирования

Тестирование Server Components

Server Components — это обычные async-функции, возвращающие JSX. Их можно тестировать через рендеринг результата:

// __tests__/product-list.test.tsx
import { render, screen } from "@testing-library/react";
import ProductList from "@/app/products/page";

// Мокаем базу данных
jest.mock("@/lib/database", () => ({
  db: {
    product: {
      findMany: jest.fn().mockResolvedValue([
        { id: 1, name: "Товар 1", price: 1000 },
        { id: 2, name: "Товар 2", price: 2000 },
      ]),
    },
  },
}));

test("отображает список товаров", async () => {
  const Component = await ProductList();
  render(Component);

  expect(screen.getByText("Товар 1")).toBeInTheDocument();
  expect(screen.getByText("Товар 2")).toBeInTheDocument();
});

Тестирование Client Components

Client Components тестируются стандартно через React Testing Library:

// __tests__/product-filter.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { ProductFilter } from "@/components/product-filter";

test("переключает фильтр категории", () => {
  render(<ProductFilter categories={["Электроника", "Одежда"]} />);

  const button = screen.getByText("Электроника");
  fireEvent.click(button);

  expect(button).toHaveClass("bg-blue-600");
});

Интеграционное тестирование

Для тестирования взаимодействия серверных и клиентских компонентов используйте E2E-фреймворки (Playwright, Cypress) и Next.js test mode.

Заключение

Server Components и Client Components — это не противоположности, а взаимодополняющие инструменты. Вот краткая сводка правил:

  1. По умолчанию — Server Component. Не добавляйте 'use client' без необходимости.
  2. Минимизируйте клиентские границы. Выносите интерактивность в маленькие leaf-компоненты.
  3. Передавайте данные через пропсы. Загружайте в серверном родителе, используйте в клиентском потомке.
  4. Используйте children-паттерн. Он позволяет сохранить серверный контент внутри клиентской обёртки.
  5. Сериализуйте всё. Передавайте только примитивы и простые объекты.

Освоение этих паттернов — один из самых значимых навыков для современного React-разработчика. Если вашему проекту нужна экспертная React-разработка с правильным использованием Server Components, обращайтесь к нашей команде.

Рекомендуем также изучить наш гайд по Server Actions — они тесно связаны с Server Components и открывают новые возможности для работы с данными.

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Содержание
  • Зачем нужно разделение на серверные и клиентские компоненты
  • Что такое Server Components
  • Что такое Client Components
  • Чек-лист для принятия решений
  • Паттерны композиции
  • Типичные ошибки
  • Влияние на производительность
  • Стратегии тестирования
  • Заключение
Поделиться:

Похожие статьи

React Server Actions: практический гайд для продакшена
React / Next.js

React Server Actions: практический гайд для продакшена

13 мин
Оптимизация Next.js: от 50 до 100 баллов PageSpeed за 1 день
React / Next.js

Оптимизация Next.js: от 50 до 100 баллов PageSpeed за 1 день

14 мин
Next.js 16: что нового и как использовать в production
React / Next.js

Next.js 16: что нового и как использовать в production

14 мин
Feature IT

Feature IT — платформа по обучению программированию и разработке цифровых продуктов. Мы создаём современные веб-решения для бизнеса и обучаем этому других!

Политика конфиденциальностиПользовательское соглашение

О компании

  • Блог
  • Карьера

Услуги разработки

  • Разработка сайтов под ключ
  • Веб-приложения на React/Next.js
  • Telegram-боты для бизнеса
  • Mini Apps (Telegram, VK)
  • SEO-оптимизированные сайты
  • Автоматизация бизнес-процессов
  • Поддержка и развитие IT-продуктов

Обучение

  • Курс Python с нуля
  • Алгоритмы и структуры данных
  • Паттерны проектирования
  • Подготовка к собеседованиям в IT
  • Практика на реальных проектах

Инструменты

  • Генератор UTM-меток
  • Счётчик символов