Серверные компоненты рендерятся только на сервере (при сборке или на каждый запрос), могут быть async и напрямую читать базу данных или файлы, а в браузер попадают как сериализованный UI без JavaScript. Клиентские компоненты ("use client") тоже рендерятся в HTML на сервере, а затем гидрируются и работают в браузере.
Серверные компоненты: без стейта, эффектов, обработчиков событий и браузерных API; их код и зависимости никогда не попадают в бандл, поэтому Markdown-парсер, использованный в таком компоненте, ничего не стоит клиенту.
Клиентские компоненты: useState, useEffect, обработчики событий и браузерные API; их код отправляется в браузер.
По умолчанию: в Next.js App Router каждый компонент серверный, пока не встретится граница "use client".
Композиция: серверный компонент может отрендерить клиентский и передать ему сериализуемые пропсы или другие серверные компоненты через children; клиентский компонент не может импортировать серверный.
RSC относится к возможностям React, но для бандлера и серверной части нужен фреймворк: главный продакшен-вариант даёт Next.js App Router, поддержку добавляют также React Router и Waku.
ReactSenior
Server Components и Next.js
React Server Components, серверные функции, CSR, SSR, SSG и ISR, стриминг, ошибки гидратации, App Router в Next.js, кэширование
Закрыто вопросов
0 / 22
- 0Повторить
- 13Часто спрашивают
Вопросы
Они отличаются тем, где и когда получается HTML: CSR собирает его в браузере из JavaScript, SSR на сервере для каждого запроса, SSG один раз при сборке, а ISR при сборке с фоновой перегенерацией страниц по таймеру или по запросу.
CSR (SPA на Vite): просто и дёшево хостить, но пустая страница, пока не загрузится JavaScript, и слабое SEO без пререндера. Подходит для дашбордов за логином.
SSR: свежий персонализированный HTML и хорошее SEO; на каждый запрос тратится серверное время и растёт время до первого байта. Подходит для страниц, зависящих от cookies или живых данных.
SSG: самый быстрый вариант, отдаётся с CDN, но контент устаревает до следующей сборки. Подходит для документации, блогов и маркетинговых страниц.
ISR: SSG плюс ревалидация по времени или по запросу, поэтому каталог товаров остаётся в кэше, но обновляется без полной пересборки.
В Next.js App Router выбор делается для каждого роута, а с Cache Components (Next.js 16) для каждого компонента: статическая оболочка пререндерится, а динамические части приходят потоком.
В App Router каждая папка внутри app/ соответствует сегменту URL, а специальные файлы в ней задают UI этого сегмента: page.tsx делает его доступным по адресу, layout.tsx оборачивает его и сохраняется между переходами, а loading.tsx и error.tsx добавляют вокруг него границу Suspense и error boundary.
layout: сохраняет стейт и не ререндерится при переходах между дочерними страницами; корневой app/layout.tsx обязан рендерить <html> и <body>.
template: похож на layout, но монтируется заново при каждом переходе.
loading: оборачивает страницу в <Suspense>, поэтому скелетон появляется сразу, пока страница приходит потоком.
error: обязан быть клиентским компонентом и получает error и reset; ошибки из layout того же сегмента он не ловит (их ловит error.tsx родителя), а корневой layout покрывает global-error.tsx.
not-found: рендерится, когда код вызывает notFound() или ни один роут не подошёл.
Папки: [id] задаёт динамический сегмент, (group) группирует файлы без изменения URL, _private исключается из роутинга, @slot задаёт параллельный роут; route.ts описывает HTTP-обработчик вместо страницы.
"use client" в начале файла помечает границу в графе модулей: этот модуль и всё, что он импортирует, становятся клиентским кодом, который попадает в бандл и гидрируется в браузере. Это не значит «рендерить только в браузере»: клиентские компоненты всё равно рендерятся в HTML на сервере.
Ставьте директиву на листья, которым нужна интерактивность (кнопка, форма, карусель), а не на страницу или layout, иначе всё поддерево и его зависимости уйдут в бандл.
Клиентский компонент может показывать контент, отрендеренный на сервере: принимайте children или другие пропсы типа ReactNode и позвольте серверному компоненту их заполнить.
Директива нужна только файлу на границе; модули, которые он импортирует, становятся клиентскими автоматически. Файл без директивы считается общим и выполняется на той стороне, которая его импортировала.
Сторонний компонент, который использует хуки, но не содержит директивы, нужно обернуть файлом в одну строку с "use client", который его реэкспортирует.
"use server" не является противоположностью: эта директива помечает серверные функции, которые можно вызывать с клиента, а не серверные компоненты.
Пропсы, которые пересекают границу, должны сериализоваться средствами React: примитивы, обычные объекты и массивы, Date, Map, Set, типизированные массивы, промисы, JSX и серверные функции. Обычные функции, экземпляры классов и символы, не зарегистрированные через Symbol.for, вызывают ошибку.
Функции: «Functions cannot be passed directly to Client Components», если только функция не серверная ("use server"); такая передаётся как ссылка, которую клиент может вызвать.
Экземпляры классов: Decimal из Prisma, сущность ORM или ваш собственный класс сначала нужно превратить в обычные данные.
Промисы: передайте промис без await и прочитайте его на клиенте через use(promise) внутри Suspense, чтобы сервер не ждал эти данные.
JSX: children и другие пропсы-элементы рендерятся на сервере и отправляются как UI, а не как код.
Размер: каждый пропс встраивается в RSC payload и в HTML, поэтому передавайте нужные клиенту поля, а не целые записи с приватными колонками.
CartPage является серверным компонентом, а серверный компонент не может навешивать обработчики событий: функцию нельзя сериализовать в RSC payload, и ни строчки кода этого компонента не выполняется в браузере, чтобы её вызвать.
У серверных компонентов нет интерактивности: ни onClick, ни useState, ни эффектов.
Исправление: вынести интерактивную часть в маленький клиентский компонент и передать ему только сериализуемые пропсы, например cartId.
Добавить "use client" на всю страницу будет неверным решением: страница и всё, что импортирует getCart, уйдут в бандл, а async клиентские компоненты не поддерживаются.
Для настоящей мутации вроде оформления заказа кнопка может вызвать серверную функцию, или <form action={checkout}> сработает даже до гидратации.
Код, который импортируют только серверные компоненты, на клиент не попадает, но один случайный импорт из клиентского компонента затянет его в бандл. Добавьте import 'server-only' в модули, которые работают с секретами или базой данных, и такой импорт сломает сборку вместо утечки.
Переменные окружения: Next.js встраивает в клиентский код только переменные с префиксом NEXT_PUBLIC_; остальные там равны undefined, что скрывает ошибку, а не сообщает о ней.
**client-only работает зеркально: сборка падает, если модуль только для браузера оказался на сервере.
Пропсы сливают данные**: всё, что серверный компонент передаёт клиентскому, сериализуется в страницу; выбирайте нужные поля и никогда не передавайте строку пользователя с passwordHash.
Слой доступа к данным: держите обращения к базе и проверки авторизации в server-only модулях, которые возвращают DTO, тогда любой путь проходит через одни и те же проверки.
Taint API в React (experimental_taintObjectReference, experimental_taintUniqueValue) позволяет запретить передачу конкретного объекта или токена на клиент; пока они экспериментальные.
Server Function представляет собой async-функцию с директивой "use server", которая выполняется на сервере, но вызывается с клиента: React отправляет аргументы POST-запросом и возвращает результат. Переданная в action формы, она называется Server Action и работает даже до загрузки JavaScript.
Объявление: "use server" в начале файла (каждый экспорт становится серверной функцией) или первой строкой async-функции внутри серверного компонента.
Формы: <form action={createTodo}> отправляет FormData; useActionState даёт возвращённый стейт и флаг isPending, а useFormStatus передаёт состояние отправки кнопке внутри формы.
После записи: вызовите revalidatePath или revalidateTag (или updateTag в Next.js 16), чтобы следующий рендер показал свежие данные; redirect() выполняет переход.
Аргументы и результаты должны быть сериализуемыми, как пропсы от сервера к клиенту.
Они предназначены для мутаций; чтению данных место в серверных компонентах, потому что клиент выполняет actions по одному, а их результаты не кэшируются.
Файл route.ts экспортирует функции с именами HTTP-методов (GET, POST, DELETE), которые принимают Request и возвращают Response, то есть описывает обычный HTTP-эндпоинт по этому URL. Используйте его, когда сервер вызывает что-то кроме вашего собственного UI; для мутаций из своих форм и кнопок берите Server Action.
Route Handler подходит для вебхуков от платёжного провайдера или GitHub, публичного или мобильного API, OAuth-колбэков, скачивания файлов и стримов, выгрузок RSS или CSV и любого GET-эндпоинта, ведь Server Actions работают только через POST.
Server Action подходит для формы или кнопки в вашем приложении, которая меняет данные. Ему не нужны URL и код fetch, он работает до гидратации, а его revalidatePath или updateTag обновляет страницу за тот же раунд-трип.
Не для своих страниц: Server Component вызывает функцию данных напрямую. Запрос к собственному Route Handler из него добавляет лишний HTTP-переход, а во время сборки отвечать некому.
Правила: в одной папке не может быть одновременно page.tsx и route.ts; начиная с Next.js 15 params является промисом; GET-обработчики с 15-й версии по умолчанию динамические, а кэширование включается явно (export const dynamic = 'force-static' или revalidate).
Та же работа по безопасности, что и у action: аутентификация и валидация входных данных. Для вебхука проверяйте подпись по сырому телу из request.text() до разбора JSON.
proxy.ts в корне проекта экспортирует одну функцию, которая выполняется до того, как запрос дойдёт до маршрута, и может сделать редирект или rewrite, выставить заголовки и cookie или ответить сама. Next.js 16 переименовал middleware в proxy и запускает его на рантайме Node.js; держите в нём только быстрые решения о запросе.
Хорошие применения: отправить посетителя без cookie сессии на /login (оптимистичная проверка), выбрать локаль, назначить группу A/B-теста, добавить заголовки вроде nonce для CSP. Фиксированные редиректы лучше задать в redirects в next.config.
Не настоящая авторизация: документация прямо говорит, что он не подходит для полноценного управления сессиями и авторизации. Проверяйте права ещё раз в каждой странице, Route Handler и Server Action: изменение matcher может пропустить путь, а в марте 2025 года CVE-2025-29927 позволяла поддельным заголовком x-middleware-subrequest полностью обойти middleware (исправлено в 15.2.3).
Никакой медленной работы: он выполняется на каждый подходящий запрос, включая prefetch, поэтому запрос к базе в нём замедляет каждую навигацию.
Никакого общего состояния: его могут развернуть отдельно от приложения, поэтому нельзя полагаться на глобальные переменные модулей; передавайте данные дальше через заголовки запроса, cookie или URL.
**Ограничьте его через config.matcher**: без него он выполняется и для _next/static, картинок и файлов из public/, и редирект там ломает CSS и JS.
Миграция: npx @next/codemod@canary middleware-to-proxy . переименует файл и функцию; middleware.ts ещё работает для Edge runtime, но объявлен устаревшим.
Сделать компонент async и напрямую делать await данных (запрос к базе, ORM или fetch), без useEffect и без клиентского API-слоя. Водопад появляется, когда await идут один за другим, поэтому независимые запросы запускайте вместе через Promise.all, а медленные части отдавайте потоком за Suspense.
Случайная последовательность: await getUser(), а затем await getOrders() складывают задержки, хотя запросы друг от друга не зависят.
Водопад родитель и потомок: дочерний компонент начинает загрузку только после того, как завершился await в родителе; запустите запрос в родителе и передайте промис вниз или дайте каждому загружать данные независимо.
Дублирующиеся вызовы писать можно: вызовы fetch с тем же URL и опциями дедуплицируются в пределах одного запроса, а cache() из React делает то же для обращений к базе.
Потоковая отдача: оберните медленную секцию в Suspense, и остальная страница уйдёт первой.
Клиентские данные (поллинг, бесконечный скролл, данные, которые пользователь редактирует на месте) по-прежнему относятся к TanStack Query или SWR в клиентских компонентах.
При стриминге сервер отправляет HTML частями в одном ответе: сначала оболочку с fallback-ами Suspense, затем каждую приостановленную часть по мере готовности, в виде HTML и маленького встроенного скрипта, который ставит её на место. Пользователь видит контент раньше, чем закончится самый медленный запрос.
Механизм: renderToReadableStream (или renderToPipeableStream в Node) сразу отдаёт всё, что вне ожидающих границ; содержимое каждой границы приходит позже в том же ответе.
Выборочная гидратация: React гидрирует границы по мере прихода их HTML и кода, а ту, с которой взаимодействует пользователь, гидрирует первой.
**loading.tsx** служит границей Suspense вокруг сегмента страницы, поэтому при переходе скелетон появляется сразу.
Статус и заголовки уходят с первым чанком: notFound() или ошибка после начала стрима уже не может поменять статус на 404 или 500, поэтому Next.js вместо этого добавляет мета-тег noindex.
Расстановка: одна граница на каждую независимую медленную секцию; если границ мало, самый медленный запрос блокирует всё, если много, страница появляется по кусочкам.
Прокси и хостинги, которые буферизуют ответы, незаметно отключают стриминг.
Гидратация подключает React к HTML, отрендеренному на сервере, и ожидает, что первый клиентский рендер даст ту же разметку. Несовпадение возникает, когда результат рендера зависит от того, что на сервере и в браузере разное: время, случайность, window, localStorage, локаль или невалидная вложенность HTML.
Типичные причины: Date.now() или toLocaleString() (другой часовой пояс и локаль), id из Math.random(), ветки с typeof window, чтение localStorage во время рендера, расширения браузера, которые добавляют атрибуты, <div> внутри <p>.
Что делает React: пишет в консоль разницу и заново рендерит на клиенте всё до ближайшей границы Suspense (или корня), выбрасывая там серверный HTML; это стоит времени и может мигнуть.
Исправления: рендерить одно и то же на обеих сторонах и переключаться на браузерное значение после монтирования в useEffect; брать id из useId; передавать время или локаль с сервера; загружать виджеты только для браузера через next/dynamic с ssr: false из клиентского компонента.
**suppressHydrationWarning** глушит расхождение в тексте или атрибуте одного элемента; он для неизбежных случаев вроде временной метки, а не универсальное исправление.
Первый рендер читает localStorage только в браузере, поэтому сервер рендерит «Dark mode», а браузер пользователя с тёмной темой при гидратации рендерит «Light mode», и текст не совпадает.
Проверка typeof window не делает сервер и клиент согласованными; как раз она и создаёт расхождение.
Первый клиентский рендер обязан совпасть с серверным HTML; значения, доступные только в браузере, применяются после гидратации, в эффекте.
Исправление: начать с безопасного для сервера значения по умолчанию и прочитать сохранённое значение в useEffect; подпись обновится сразу после гидратации.
Чтобы избежать даже этого короткого переключения темы, храните выбор в cookie, которую читает сервер, или ставьте класс на <html> крошечным встроенным скриптом до загрузки React и стилизуйте через CSS.
SPA стоит выбрать, когда SEO и скорость первой загрузки для анонимных посетителей не важны, а приложение очень интерактивное и живёт за логином: админка, внутренний инструмент, редактор. Оно деплоится статическими файлами на любой CDN, без Node-сервера.
Сильные стороны SPA: модель проще (всё выполняется в браузере), дешёвый хостинг, офлайн через service worker, не нужно проектировать границу между сервером и клиентом.
Сильные стороны Next.js и RSC: HTML с сервера для SEO и LCP, меньше JavaScript на контентных страницах, данные загружаются рядом с базой, стриминг.
Цена RSC: сервер, который нужно поддерживать и оплачивать, привязка к фреймворку, слои кэширования, в которых надо разобраться, и правила сериализации на каждой границе.
Промежуточный вариант: React Router v7 в режиме фреймворка и TanStack Start дают SSR и загрузчики данных без перехода на Next.js; SPA также может пререндерить свои публичные страницы.
create-react-app объявлен устаревшим (с февраля 2025); новые SPA начинают с Vite.
Экспортируйте из page или layout статический объект metadata или асинхронную generateMetadata, если теги зависят от параметров или данных; Next.js превращает их в теги <title>, <meta> и <link> в серверном HTML. Специальные файлы (sitemap.ts, robots.ts, opengraph-image.tsx) генерируют остальное.
Заголовки: title: { template: '%s | Shop', default: 'Shop' } в корневом layout, чтобы каждая страница задавала только свою часть.
Canonical и языки: metadataBase в корневом layout позволяет писать URL-поля относительными, а alternates: { canonical, languages } выводит канонический link и альтернативы hreflang.
Без двойной загрузки: вызовы fetch мемоизируются между generateMetadata и страницей в пределах одного запроса; обращения к базе оборачивайте в cache() из React ради того же эффекта.
Слияние поверхностное: страница, которая задаёт openGraph, заменяет весь объект openGraph из layout, поэтому общие поля вроде картинки пропадают, если не подмешать их обратно через spread.
Только Server Components: страница с "use client" не может экспортировать метаданные; оставьте страницу на сервере, а интерактивную часть вынесите в дочерний компонент.
Стриминг метаданных (Next.js 15.2+): краулеры, которые выполняют JavaScript, например Googlebot, могут получить теги после первого чанка HTML, а боты, читающие только HTML, например генераторы превью ссылок, получают их в <head>.
Одних тегов мало: сам контент должен быть в серверном HTML, а отсутствующая страница должна вызывать notFound(), чтобы отдавать настоящий 404.
React превращает каждую серверную функцию в HTTP-эндпоинт с id, который клиент отправляет в POST-запросе, и вызвать его может кто угодно с любыми аргументами, а не только ваша форма. Поэтому каждый action сам обязан проверять аутентификацию, права и входные данные, ровно как API-роут.
Авторизация внутри action: проверяйте сессию в самой функции; если спрятать кнопку от не-админов, это ничего не защищает, и проверка в middleware или proxy.ts её не заменяет.
Права на объект: вопрос в том, может ли этот пользователь менять именно этот postId, а не просто залогинен ли кто-то; id приходят с клиента и их можно подменить.
Валидация входа схемой (Zod, Valibot): значения FormData приходят строками или файлами любого размера, а типы TypeScript во время выполнения не проверяются.
Замыкания: значения, которые захватывает инлайн-action, путешествуют на клиент и обратно; Next.js их шифрует, но секреты захватывать нельзя.
Встроенные защиты: Next.js принимает только POST, сравнивает Origin с Host против CSRF, выкидывает неиспользуемые actions из сборки и делает их id неугадываемыми; ничто из этого не заменяет авторизацию.
Ограничивайте частоту вызовов (rate limit) для дорогих и уязвимых к злоупотреблениям actions (регистрация, отправка писем).
Next.js кэширует на нескольких уровнях: мемоизация запросов в пределах одного рендера, Data Cache для результатов между запросами, Full Route Cache для пререндеренных страниц и клиентский Router Cache. С версии 15 данные по умолчанию не кэшируются, а Cache Components в 16 делают кэширование явным через "use cache".
Уровни
Мемоизация запросов: одинаковые вызовы fetch и функции, обёрнутые в cache из React, выполняются один раз на серверный запрос.
Data Cache: сохранённые результаты fetch, включаются через cache: 'force-cache' или next: { revalidate } и помечаются тегами через next: { tags }.
Full Route Cache: HTML и RSC payload статических роутов, собранные при билде или при ревалидации.
Router Cache: RSC payload посещённых и предзагруженных сегментов, который хранится в браузере для переходов назад и вперёд.
Что изменилось
Next.js 14 кэшировал fetch по умолчанию, и многие команды неожиданно получали устаревшие данные.
Next.js 15 перестал по умолчанию кэшировать fetch, GET-обработчики роутов и сегменты страниц в клиентском Router Cache; кэширование включается явно.
Next.js 16 с cacheComponents: true: всё динамическое, пока не помечено "use cache" на уровне файла, компонента или функции, с cacheLife для длительности и cacheTag для инвалидации; статическая оболочка пререндерится, а динамические части приходят потоком (Partial Prerendering).
На собеседовании
Говорите, что кэшируется, как долго, по какому ключу и как инвалидируется; большинство багов с устаревшими данными сводятся к пропущенному тегу или забытому уровню кэша.
Инвалидировать записи кэша, которые запись в базу сделала устаревшими, прямо из серверной функции, которая её выполнила: revalidateTag для всех закэшированных чтений с этим тегом, revalidatePath для одного роута. В Next.js 16 updateTag сразу сбрасывает тег, и пользователь видит свою запись при следующем рендере.
**revalidateTag(tag, profile)**: помечает записи с тегом устаревшими и перезагружает их в фоне (stale-while-revalidate); в Next.js 16 второй аргумент, например 'max', обязателен. Подходит для контента, который другие увидят чуть позже.
**updateTag(tag) (Next.js 16, только в Server Actions): сбрасывает записи и перезагружает их в том же запросе, что даёт read-your-own-writes.
revalidatePath('/posts')**: инвалидирует один роут (или всё под layout с 'layout'), это грубее тегов.
**refresh()** из next/cache в Server Action или router.refresh() на клиенте заново рендерит серверные компоненты текущего роута, не трогая закэшированные данные.
При вызове внутри Server Action они также обновляют клиентский Router Cache, и страница показывает новые данные за тот же запрос.
Ставьте теги по сущности (post-42) и по коллекции (posts), чтобы одна запись инвалидировала ровно то, что изменила.
cache(fn) из React мемоизирует функцию на время одного серверного запроса: компоненты, которые вызывают getUser(id) с одинаковыми аргументами, делят один вызов и один результат. "use cache" сохраняет результаты между запросами и пользователями, пока они не истекут или не будут инвалидированы.
**Зачем cache**: глубоко вложенные компоненты загружают нужные им данные без prop drilling и без повторных запросов к базе; fetch дедуплицируется автоматически, вызовы ORM нет.
Область действия: мемо выбрасывается после запроса, поэтому данные одного пользователя никогда не попадут другому.
Аргументы: объекты сравниваются по ссылке, поэтому передавайте примитивы; новый объект при каждом вызове промахивается мимо кэша.
Объявляйте на уровне модуля, а не внутри компонента, иначе каждый рендер получит новый кэш.
Паттерн preload: вызовите кэшированную функцию заранее без await, и запрос начнётся раньше, чем отрендерится компонент, который его ждёт.
**"use cache"** требует сериализуемых аргументов и результатов и не должен читать cookies() или headers() внутри; прочитайте их снаружи и передайте значения.
currentUser живёт в области видимости модуля, а серверный модуль загружается один раз на процесс и используется всеми запросами: первый посетитель после старта кэшируется, и все последующие на этом инстансе получают его имя. В разработке всё работает, потому что там вы единственный пользователь.
Почему только иногда: каждый серверный инстанс или serverless-функция хранит того, кто пришёл первым, до перезапуска, поэтому при нескольких инстансах разные пользователи видят разных чужаков, а каждый деплой их перетасовывает.
Задумывалась дедупликация в рамках запроса, и именно её даёт cache() из React: мемо живёт один запрос и выбрасывается после него, поэтому layout, шапка и страница делят один запрос к базе, не делясь им с другими пользователями.
Тот же класс багов: стор, QueryClient или экземпляр i18n на уровне модуля на сервере, а также переменная модуля, которую задаёт layout и читает дочерний компонент: конкурентный запрос может перезаписать её между await и чтением.
Общие кэши без пользователя в ключе дают тот же симптом: кэшируемая функция (например, unstable_cache с фиксированным ключом), которая возвращает личные данные, или правило CDN (Cache-Control: public) на персонализированной странице.
Состояние запроса принадлежит запросу: cookies() и headers() из next/headers привязаны к текущему запросу; читайте их внутри функции, которой нужен пользователь.
RSC payload (формат «Flight») представляет собой потоковую сериализацию отрендеренного дерева: элементы, текст, пропсы клиентских компонентов и ссылки на их JavaScript-чанки вместо самого кода. При первой загрузке клиент гидрирует с ним HTML, а при навигации обновляет дерево на месте.
Первая загрузка: сервер рендерит payload в HTML для быстрой первой отрисовки и ещё встраивает payload в теги <script>, чтобы клиент мог гидрироваться, не загружая его повторно.
Навигация: роутер загружает только payload изменившихся сегментов (запрос с RSC: 1 в Next.js, без HTML) и применяет его к существующему дереву, поэтому клиентский стейт в общих layout сохраняется.
Клиентские ссылки: каждый клиентский компонент представлен id модуля и именем экспорта; браузер загружает этот чанк и рендерит его с сериализованными пропсами.
Стриминг: ожидающие границы Suspense становятся заглушками, которые заполняют следующие строки того же потока.
Цена: при первой загрузке пропсы отправляются дважды, в HTML и в payload, поэтому передача целых записей в клиентские компоненты раздувает страницы.