React Server Components: как работают серверные компоненты в React 19 и Next.js

Чем серверные компоненты отличаются от SSR, загрузка данных и стриминг, кэширование с use cache в Next.js 16, Server Actions, уязвимость React2Shell и её последствия, переход с Pages Router на App Router и типичные ошибки.

React Server Components, или RSC, — это компоненты React, которые выполняются только на сервере и не попадают в JavaScript, который скачивает браузер. Они могут напрямую читать базу данных и файлы, использовать секретные ключи и тяжёлые библиотеки, а пользователю отправляют уже готовый результат. Интерактивные части интерфейса остаются обычными клиентскими компонентами. С выходом React 19 в декабре 2024 года серверные компоненты стали стабильной частью React, а в Next.js на App Router это модель по умолчанию. Ниже — как RSC устроены, чем отличаются от привычного серверного рендеринга, как в актуальных версиях Next.js работают загрузка данных, кэширование и Server Actions, какие ошибки встречаются при переходе и что нужно знать о безопасности после уязвимости React2Shell. Чем RSC отличаются от SSR и клиентского рендеринга Эти понятия часто смешивают, хотя они решают разные задачи. Клиентский рендеринг (CSR). Сервер отдаёт почти пустой HTML и бандл JavaScript, интерфейс собирается в браузере. Первый показ содержимого зависит от скорости загрузки и выполнения скриптов. Серверный рендеринг (SSR). Сервер отдаёт готовый HTML, но код всех компонентов всё равно скачивается в браузер, и React выполняет гидратацию — заново проходит по дереву, чтобы подключить обработчики событий. Серверные компоненты (RSC). Часть компонентов выполняется только на сервере. Их код не попадает в клиентский бандл и не гидратируется: в браузер уходит сериализованный результат рендеринга, так называемый RSC Payload. Клиентские компоненты внутри дерева гидратируются как обычно. RSC не заменяют SSR, а работают вместе с ним: в Next.js первая загрузка страницы приходит как HTML, построенный из серверных и клиентских компонентов, а при переходах между страницами клиент получает RSC Payload и обновляет интерфейс без полной перезагрузки. Главный практический эффект — в браузер не отправляется код, который нужен только для отображения данных: форматирование, разбор Markdown, работа с датами, запросы к базе. Насколько уменьшится бандл, зависит от приложения: страницы, которые в основном показывают данные, выигрывают сильно, насыщенные интерактивом редакторы — слабо. Серверные и клиентские компоненты В App Router Next.js каждый компонент по умолчанию серверный. Клиентским его делает директива 'use client' в начале файла — она отмечает границу, после которой компонент и всё, что он импортирует, попадает в клиентский бандл. // app/orders/page.tsx — серверный компонент import { db } from '@/lib/db'; import { OrdersFilter } from './OrdersFilter'; export default async function OrdersPage() { const orders = await db.order.findMany({ orderBy: { createdAt: 'desc' }, take: 50, }); return ( section h1 Заказы /h1 OrdersFilter statuses={['new', 'paid', 'shipped']} / ul {orders.map((order) = ( li key={order.id} №{order.number} — {order.status} /li ))} /ul /section ); } // app/orders/OrdersFilter.tsx — клиентский компонент 'use client'; import { useRouter, useSearchParams } from 'next/navigation'; export function OrdersFilter({ statuses }: { statuses: string[] }) { const router = useRouter(); const params = useSearchParams(); return ( select value={params.get('status') ?? ''} onChange={(e) = router.push(`?status=${e.target.value}`)} option value="" Все /option {statuses.map((s) = option key={s} value={s} {s} /option )} /select ); } Что можно и чего нельзя в серверных компонентах Можно: делать компонент асинхронным, обращаться к базе данных и внутренним сервисам, читать переменные окружения с секретами, использовать тяжёлые библиотеки без влияния на бандл. Нельзя: хуки состояния и эффектов ( useState , useEffect ), обработчики событий, браузерные API ( window , localStorage ), React Context в роли провайдера. Чтобы серверный модуль с доступом к базе случайно не оказался в клиентском бандле, в его начало добавляют import 'server-only' — при попытке импорта из клиентского компонента сборка упадёт с ошибкой. Что можно передавать через границу Пропсы из серверного компонента в клиентский сериализуются. Передавать можно примитивы, простые объекты и массивы, даты, Map и Set , JSX-элементы, промисы, которые клиентский компонент прочитает через use , и серверные функции, помеченные 'use server' . Нельзя передавать обычные функции, экземпляры классов и символы. // Нельзя: обычная функция не сериализуется PriceTag format={(value) = formatRub(value)} / // Можно: данные, а форматирование — внутри клиентского компонента PriceTag amount={1500000} currency="RUB" / Загрузка данных и стриминг Параллельные запросы вместо водопада Последовательные await в серверном компоненте создают тот же водопад запросов, что и на клиенте. Независимые данные запрашиваются параллельно. // app/products/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise { id: string } ; }) { const { id } = await params; const [product, stock] = await Promise.all([ getProduct(id), getStock(id), ]); return ProductCard product={product} stock={stock} / ; } Обратите внимание на params : начиная с Next.js 15 это промис, а в Next.js 16 синхронный доступ к params , searchParams , cookies() и headers() удалён окончательно. Suspense: показывать страницу по частям Медленные блоки оборачиваются в Suspense . Сервер сразу отправляет основную часть страницы с заглушками, а готовые фрагменты досылает в том же ответе по мере готовности. import { Suspense } from 'react'; export default async function ProductPage({ params }: { params: Promise { id: string } }) { const { id } = await params; const product = await getProduct(id); // критичные данные return ( ProductHeader product={product} / Suspense fallback={ ReviewsSkeleton / } Reviews productId={id} / /Suspense Suspense fallback={ RelatedSkeleton / } RelatedProducts productId={id} / /Suspense / ); } Пользователь видит карточку товара, не дожидаясь отзывов и рекомендаций. Файл loading.tsx в папке маршрута оборачивает всю страницу в Suspense автоматически и показывает заглушку при переходах. Кэширование в актуальном Next.js Кэширование — область, которая менялась сильнее всего, поэтому многие руководства устарели. В Next.js 13 и 14 запросы fetch в серверных компонентах кэшировались по умолчанию, и разработчики часто получали устаревшие данные неожиданно для себя. В Next.js 15 умолчание поменяли: запросы fetch больше не кэшируются, если это не указано явно. В Next.js 16, вышедшем в октябре 2025 года, появилась модель Cache Components: кэширование полностью явное и задаётся директивой 'use cache' на уровне функции, компонента или файла. // next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { cacheComponents: true, }; export default nextConfig; // lib/catalog.ts import { cacheLife, cacheTag } from 'next/cache'; export async function getCategories() { 'use cache'; cacheLife('days'); cacheTag('categories'); return db.category.findMany({ orderBy: { position: 'asc' } }); } Сбрасывать кэш после изменения данных можно двумя способами. updateTag(tag) вызывается в Server Action и сразу отдаёт свежие данные — подходит, когда пользователь должен немедленно увидеть свою правку. revalidateTag(tag, 'max') помечает данные устаревшими и обновляет их в фоне: следующий посетитель получит кэшированную версию, а свежая соберётся параллельно. Вызов revalidateTag с одним аргументом в Next.js 16 объявлен устаревшим. Внутри функций с 'use cache' нельзя обращаться к cookies() и headers() : значения нужно прочитать снаружи и передать аргументами. Иначе персональные данные одного пользователя рискуют попасть в общий кэш. Server Actions: изменение данных без API-роутов Серверные функции, отмеченные 'use server' , можно вызывать из форм и клиентских компонентов. В React они называются Server Functions, в Next.js — Server Actions. Фреймворк сам создаёт для них сетевую точку входа и сериализует аргументы. // app/contact/actions.ts 'use server'; import { db } from '@/lib/db'; import { updateTag } from 'next/cache'; type State = { error?: string; ok?: boolean } | null; export async function submitLead(prevState: State, formData: FormData): Promise State { const name = String(formData.get('name') ?? '').trim(); const email = String(formData.get('email') ?? '').trim(); const message = String(formData.get('message') ?? '').trim(); if (name.length 2 || !email.includes('@') || message.length 20) { return { error: 'Проверьте имя, почту и описание задачи' }; } await db.lead.create({ data: { name, email, message } }); updateTag('leads'); return { ok: true }; } // app/contact/LeadForm.tsx 'use client'; import { useActionState } from 'react'; import { submitLead } from './actions'; export function LeadForm() { const [state, formAction, isPending] = useActionState(submitLead, null); return ( form action={formAction} input name="name" placeholder="Имя" required / input name="email" type="email" placeholder="Почта" required / textarea name="message" placeholder="Опишите задачу" rows={5} / {state?.error p role="alert" {state.error} /p } {state?.ok p Заявка отправлена /p } button type="submit" disabled={isPending} {isPending ? 'Отправляем…' : 'Отправить'} /button /form ); } Функция, переданная в useActionState , получает первым аргументом предыдущее состояние, а вторым — данные формы. Если форма отрисована на сервере, она отправится и до загрузки JavaScript. Важно помнить: каждая Server Action — это публичная точка входа, которую можно вызвать напрямую, минуя интерфейс. Проверка прав доступа и валидация входных данных внутри действия обязательны, даже если кнопка видна только администратору. Общие принципы защиты таких точек входа разобраны в статье «API Security» . Безопасность: уязвимость React2Shell 3 декабря 2025 года команда React раскрыла критическую уязвимость CVE-2025-55182, получившую название React2Shell. Ошибка в разборе данных, приходящих на точки входа серверных функций, позволяла неаутентифицированному злоумышленнику одним HTTP-запросом выполнить произвольный код на сервере. Оценка по шкале CVSS — 10 из 10, максимальная. Затронутые версии: пакеты react-server-dom-webpack , react-server-dom-parcel и react-server-dom-turbopack версий 19.0.0, 19.1.0, 19.1.1 и 19.2.0, а значит, и фреймворки на их основе, включая Next.js с App Router. Исправления: первые патчи вышли в версиях 19.0.1, 19.1.2 и 19.2.1, Next.js выпустил обновления для всех поддерживаемых веток. Эксплуатация: как сообщали Google Threat Intelligence и Microsoft Security, атаки начались вскоре после публикации и шли от разных групп — от массовых сканеров до целевых злоумышленников. Продолжение: в декабре 2025 и январе 2026 года были раскрыты связанные уязвимости в том же механизме — отказ в обслуживании (CVE-2025-55184, CVE-2025-67779, CVE-2026-23864) и раскрытие исходного кода серверных функций (CVE-2025-55183). Поэтому первых патчей недостаточно: нужны последние версии своей ветки. Приложения, где React работает только в браузере без серверных компонентов и серверных функций, эти уязвимости не затрагивают. Для всех проектов на RSC выводы практические: держать React и фреймворк на актуальных патч-версиях, подписаться на бюллетени безопасности Next.js — они выходят регулярно, — и иметь процесс, который позволяет выкатить срочное обновление за часы, а не за недели. RSC и поисковая оптимизация Серверные компоненты отдают содержимое в HTML сразу, поэтому поисковым роботам не нужно выполнять JavaScript, чтобы увидеть текст страницы. Метаданные формируются на сервере из тех же данных, что и страница. // app/services/[id]/page.tsx import type { Metadata } from 'next'; export async function generateMetadata({ params, }: { params: Promise { id: string } ; }): Promise Metadata { const { id } = await params; const service = await getService(id); return { title: service.seoTitle, description: service.seoDescription, alternates: { canonical: `/services/${id}` }, openGraph: { title: service.name, images: [service.ogImage] }, }; } Если сайт уже работает как одностраничное приложение и плохо индексируется, переход на RSC — лишь один из вариантов решения. Все способы, от