Как работают Server Components (RSC) в Next.js
React Server Components (RSC) это компоненты, которые выполняются исключительно на сервере. Их код никогда не попадает в JavaScript-бандл, который скачивает браузер. В App Router Next.js (начиная с версии 13) все компоненты являются серверными по умолчанию.
Зачем нужны Server Components
До появления RSC весь React-код отправлялся в браузер. Даже если компонент просто отображал данные из базы, его код, зависимости и логика включались в клиентский бандл.
Server Components решают три проблемы:
-
Размер бандла. Код серверных компонентов и их зависимости не отправляются клиенту. Если ты используешь тяжелую библиотеку для обработки данных, она остается на сервере.
-
Прямой доступ к данным. Серверные компоненты могут напрямую обращаться к базе данных, файловой системе, env-переменным. Не нужен промежуточный API-слой.
-
Безопасность. Секреты (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) | ✓ |
Как это работает под капотом
Рендеринг на сервере
Next.js выполняет серверные компоненты и создает специальный формат (RSC Payload), который описывает результат рендеринга.
Потоковая передача клиенту
RSC Payload передается в браузер вместе с HTML. Клиентские компоненты гидратируются, серверные уже отрендерены.
Обновление интерфейса
При навигации 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 — паттерны комбинирования серверных и клиентских компонентов