Как работают Server Components (RSC) в Next.js

React Server Components (RSC) это компоненты, которые выполняются исключительно на сервере. Их код никогда не попадает в JavaScript-бандл, который скачивает браузер. В App Router Next.js (начиная с версии 13) все компоненты являются серверными по умолчанию.

Зачем нужны Server Components

До появления RSC весь React-код отправлялся в браузер. Даже если компонент просто отображал данные из базы, его код, зависимости и логика включались в клиентский бандл.

Server Components решают три проблемы:

  1. Размер бандла. Код серверных компонентов и их зависимости не отправляются клиенту. Если ты используешь тяжелую библиотеку для обработки данных, она остается на сервере.

  2. Прямой доступ к данным. Серверные компоненты могут напрямую обращаться к базе данных, файловой системе, env-переменным. Не нужен промежуточный API-слой.

  3. Безопасность. Секреты (API-ключи, строки подключения к БД) никогда не попадают к клиенту.

Серверные vs клиентские компоненты

// Серверный компонент (по умолчанию)
// Выполняется на сервере, код не попадает в бандл
import { db } from '@/lib/db'

export default async function ProblemsCount() {
  const count = await db.problem.count()
  return <p>Задач на Hack Frontend: {count}</p>
}
// Клиентский компонент
// Помечаем директивой 'use client'
'use client'

import { useState } from 'react'

export default function LikeButton() {
  const [liked, setLiked] = useState(false)

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? 'Убрать лайк' : 'Лайкнуть'}
    </button>
  )
}

Когда какой использовать

ЗадачаСерверныйКлиентский
Загрузка данных из БД/API
Доступ к env-переменным, файлам
Тяжелые зависимости (markdown-парсер и т.д.)
useState, useEffect, useRef
onClick, onChange, onSubmit
Browser API (localStorage, geolocation)
React Context (useContext)

Как это работает под капотом

1

Рендеринг на сервере

Next.js выполняет серверные компоненты и создает специальный формат (RSC Payload), который описывает результат рендеринга.

2

Потоковая передача клиенту

RSC Payload передается в браузер вместе с HTML. Клиентские компоненты гидратируются, серверные уже отрендерены.

3

Обновление интерфейса

При навигации Next.js запрашивает RSC Payload для нового роута. Серверные компоненты перерендериваются на сервере, клиентские сохраняют свое состояние.

Паттерны использования

Серверный компонент оборачивает клиентский

Самый распространенный паттерн: серверный компонент загружает данные и передает их клиентскому через props:

// app/problems/page.tsx (серверный)
import { db } from '@/lib/db'
import { ProblemList } from './problem-list'

export default async function ProblemsPage() {
  const problems = await db.problem.findMany({
    orderBy: { difficulty: 'asc' }
  })

  return <ProblemList problems={problems} />
}
// app/problems/problem-list.tsx (клиентский)
'use client'

import { useState } from 'react'

export function ProblemList({ problems }) {
  const [filter, setFilter] = useState('all')

  const filtered = filter === 'all'
    ? problems
    : problems.filter(p => p.difficulty === Number(filter))

  return (
    <div>
      <select onChange={e => setFilter(e.target.value)}>
        <option value="all">Все</option>
        <option value="1">Легкие</option>
        <option value="2">Средние</option>
        <option value="3">Сложные</option>
      </select>
      <ul>
        {filtered.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  )
}

Серверный компонент как children

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

// Клиентский компонент-обертка
'use client'

import { useState } from 'react'

export function Accordion({ title, children }) {
  const [open, setOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setOpen(!open)}>{title}</button>
      {open && children}
    </div>
  )
}
// Серверный компонент использует клиентскую обертку
import { Accordion } from './accordion'
import { db } from '@/lib/db'

export default async function FAQ() {
  const items = await db.faq.findMany()

  return (
    <div>
      {items.map(item => (
        <Accordion key={item.id} title={item.question}>
          <p>{item.answer}</p>
        </Accordion>
      ))}
    </div>
  )
}

Важно:

Серверный компонент нельзя импортировать внутри клиентского. Если ты добавишь 'use client' в файл, все его импорты тоже станут клиентскими. Передавай серверные компоненты только через props или children.

Ограничения Server Components

  • Нельзя использовать хуки (useState, useEffect, useContext и т.д.)
  • Нельзя добавлять обработчики событий (onClick, onChange)
  • Нельзя использовать browser API
  • Нельзя импортировать в клиентских компонентах напрямую

На собеседовании:

Частая ошибка на собеседовании: путать Server Components и SSR. SSR рендерит весь React-дерево на сервере один раз и отправляет HTML. Server Components это другая модель: часть дерева живет на сервере постоянно и никогда не отправляет свой код клиенту. При навигации серверные компоненты перерендериваются на сервере.

Полезные ресурсы

  • nextjs.org/docs — документация по серверным компонентам
  • react.dev — RSC в документации React
  • Composition Patterns — паттерны комбинирования серверных и клиентских компонентов

Связанные темы