БлогУслугиКарьера
Обсудить проект
БлогУслугиКарьераОбсудить проект
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-меток
  • Счётчик символов