Senior Frontend Architect, 10+ лет опыта построения production-проектов на Next.js. Contentful Certified Professional (2024). Специализация: React Server Components, headless eCommerce, инженерия Core Web Vitals.
React Server Components (RSC), появившиеся как экспериментальный RFC в декабре 2020 года и стабилизированные в React 19 (декабрь 2024), - самый значимый архитектурный сдвиг в React со времён хуков. RSC не слой оптимизации, прикрученный к прежней модели, а новая парадигма рендеринга, которая заново определяет, как компонуются компоненты, где запрашиваются данные и что вообще уезжает в браузер. В этом посте: таксономия рендеринга, которую RSC вытесняет, модель компонентов и формат передачи, практические паттерны, которые она открывает - асинхронные компоненты, стриминг через Suspense, частичный пререндеринг, - и фреймворк выбора с реальными бенчмарками размера бандла.
Часть I - Таксономия рендеринга до RSC и её изъяны
До RSC приложения на React работали в четырёх режимах рендеринга, и каждый был компромиссом по двум осям: где генерируется HTML (сервер или клиент) и когда запрашиваются данные (на сборке, на запросе или на клиенте). Разложить их стоит потому, что именно общий для всех четырёх изъян RSC и убирает.
- CSR (рендеринг на клиенте): HTML - пустая оболочка; приложение React стартует в браузере, тянет данные через fetch и рендерит всё на клиенте. Следствие: 100% фреймворка, дерева компонентов и бизнес-логики уезжает клиенту. Пользователь видит пустую страницу, пока бандл не разобран, не скомпилирован и не гидратирован. TTI прямо пропорционален размеру бандла.
- SSR (рендеринг на сервере): HTML генерируется на сервере на каждый запрос через
renderToStringилиrenderToPipeableStream. Браузер сразу получает осмысленный HTML, что хорошо для FCP и LCP, но всё равно обязан скачать полный бандл React и гидратировать его, навесив обработчики на серверную разметку. Стоимость JavaScript та же, что при CSR: воспринимаемая загрузка лучше, стоимость интерактивности не изменилась. Сокращение именно этой стоимости - задержки обработчика, которую пользователь ощущает, - и есть содержание оптимизации INP в Next.js. - SSG (статическая генерация): HTML пререндерится на сборке. Ноль серверных вычислений на запрос, максимальная кешируемость на краю сети. Ограничение - свежесть данных: страница товара, отрисованная на сборке, не покажет живые остатки без пересборки или ISR.
- ISR (инкрементальная статическая регенерация): Расширение SSG, специфичное для Next.js. Страницы перегенерируются по расписанию или по требованию, что даёт почти статическую отдачу с управляемой свежестью. Полный бандл гидратации всё равно уезжает клиенту.
Изъян у всех четырёх режимов общий: клиент обязан получить, разобрать, скомпилировать и выполнить полное JavaScript-представление каждого компонента на странице - независимо от того, нужна ли этому компоненту хоть какая-то интерактивность. Шапка навигации, которая читает базу и рисует статические ссылки, стоит клиенту столько же JavaScript, сколько сложная интерактивная форма. RSC убирает это, делая разделение сервер-клиент архитектурным примитивом первого класса, а не выбором на этапе развёртывания.
Часть II - Формальная модель RSC: RFC 188, типы компонентов и формат передачи
Спецификация RSC (React RFC #188, https://github.com/reactjs/rfcs/pull/188) определяет два взаимоисключающих типа компонентов внутри одного дерева React: серверные и клиентские. Классификация задаётся на уровне модуля, а не в рантайме, поэтому это статическое свойство кодовой базы, проверяемое сборщиком.
Серверный компонент - любой компонент React, в модуле которого нет директивы 'use client' сверху. Такие компоненты исполняются исключительно на сервере. Они могут быть async-функциями, могут прямо в теле рендера ждать запрос к базе или чтение файла и не добавляют в клиентский бандл ни байта. Их вывод - не HTML и не дерево VDOM, а сериализованная нагрузка в формате передачи RSC: JSON-подобном протоколе, который кодирует деревья элементов React вместе со ссылками на клиентские компоненты как непрозрачные идентификаторы модулей.
Клиентский компонент - любой компонент, модуль которого начинается директивой 'use client'. Директива размечает границу модуля, ниже которой все транзитивные импорты тоже считаются клиентским кодом. Клиентские компоненты попадают в бандл для браузера, поддерживают состояние (useState), эффекты (useEffect), браузерные API и обработчики событий. Это продолжение классической модели компонентов React.
Ключевая мысль и главный источник архитектурной путаницы: 'use client' объявляет границу, а не место рендера. Клиентские компоненты всё равно рендерятся на сервере во время SSR, чтобы получить исходный HTML; директива означает «этот компонент и его поддерево ещё и гидратируются и перерендериваются на клиенте». Серверные компоненты на клиенте не перерендериваются никогда. В пределах запроса их вывод статичен.
- Формат передачи RSC: Передаётся потоковым JSON-подобным текстом (Content-Type: text/x-component). Каждая строка - дескриптор элемента React: тип элемента (строка для DOM, ссылка на модуль для клиентского компонента), пропсы и дети. Вывод серверных компонентов встраивается как JSON; клиентские адресуются идентификатором модуля и разрешаются по манифесту чанков клиентского бандла.
- Ограничение сериализации: Пропсы, пересекающие границу сервер-клиент, обязаны сериализоваться: примитивы, обычные объекты, массивы, Date, Map, Set, промисы. Несериализуемое (функции, экземпляры классов, узлы DOM) передать из серверного компонента в клиентский нельзя. Это проверяется в рантайме в разработке и статическим анализом.
- Протокол React Flight: Внутреннее название стримингового протокола RSC. Он поддерживает доставку чанков не по порядку: серверный компонент с медленной асинхронной операцией не блокирует более быстрых соседей, потому что вывод каждого компонента стримится отдельным чанком, а рантайм на клиенте собирает их по мере прихода.
Модель композиции: чередование серверных и клиентских поддеревьев
Модель композиции RSC выглядит контринтуитивно, пока не понято лежащее в её основе ограничение: серверный компонент не может импортировать клиентский как ребёнка в свой JSX, если этот импорт затащит модуль серверного компонента в клиентский бандл. Зато серверный компонент может передать клиентский пропсом (в частности как children или любой JSX-проп), а клиентский - отрисовать свой children, которым может оказаться серверное поддерево. Это паттерн «пончика»: клиентская оболочка с серверной начинкой.
Конкретно: <ClientShell><ServerContent /></ClientShell> - валидная конструкция. ClientShell - клиентский компонент, оборачивающий свой проп children. ServerContent - серверный компонент, который остаётся на сервере. Модуль ClientShell попадает в клиентский бандл, модуль ServerContent - нет. Формат передачи RSC несёт уже отрисованный вывод ServerContent как готовое поддерево внутри пропсов ClientShell: исходный код ServerContent клиент не видит никогда, только результат.
У этого правила серьёзные последствия для проектирования библиотек. Библиотека компонентов, которая заворачивает всё в 'use client', вынуждает каждого потребителя тащить её в свой бандл. Библиотека, которая аккуратно ставит 'use client' только на листовых интерактивных элементах - кнопках, полях ввода, модалках, - позволяет использовать свои чисто презентационные компоненты (типографику, лейауты, контейнеры) как серверные и отправлять для этих поддеревьев ноль JavaScript.
Часть III - Асинхронные компоненты и архитектура запроса данных
До RSC запрос данных в React был клиентской заботой через useEffect, SWR, React Query или функции Next.js getServerSideProps и getStaticProps. Каждая модель приносила одну из двух проблем: либо данные шли с клиента, стоя дополнительного round-trip после гидратации, либо запрашивались в особой функции уровня страницы, которую нельзя было положить рядом с потребляющим компонентом.
RSC решает это, делая серверные компоненты async-функциями. Такой компонент может прямо в теле рендера ждать любую асинхронную операцию: запрос через Prisma или Drizzle, вызов fetch() к внутреннему API, чтение файла через fs.readFile. Рантайм React дожидается промиса компонента, прежде чем стримить его вывод. Это не обходной приём, а задуманная модель, зафиксированная в RFC 188 и реализованная как async function Component() в React 19.
- Защита от водопадов через параллельные запросы: Несколько соседних асинхронных серверных компонентов рендерятся серверным рантаймом параллельно. Каждый приостанавливается независимо, и рантайм продолжает с теми, что разрешились раньше. Ключевой приём - запускать все промисы до первого
await:const [a, b] = await Promise.all([fetchA(), fetchB()])внутри одного компонента, чтобы держать связанные запросы рядом и не вводить искусственную последовательность. - Дедупликация кеша: Next.js оборачивает вызовы
fetch()внутри одного прохода рендера автоматической дедупликацией: одинаковые сочетания URL и опций в пределах запроса схлопываются в один сетевой вызов. Поэтому несколько компонентов дерева могут независимо зватьfetch('/api/user'), не порождая N запросов. - Модель запроса и ответа: В App Router каждый рендер RSC связан с контекстом запроса, доступным через
headers(),cookies()иparams. Это заменяет аргументcontextизgetServerSidePropsи открывает доступ к данным запроса любому компоненту дерева, а не только корню страницы.
Suspense, стриминг и протокол прогрессивного рендеринга
Компонент Suspense нативно работает с асинхронными серверными компонентами. Когда серверный компонент ждёт неразрешённую операцию, React приостанавливает это поддерево и рисует вместо него ближайшую границу <Suspense fallback={...}>. По мере разрешения промиса готовое поддерево уезжает клиенту новым чанком RSC, и React подставляет его на место заглушки.
Транспортом для стриминга служит chunked transfer encoding в HTTP/1.1 или мультиплексированные потоки HTTP/2. Сервер начинает отдавать документ сразу по получении запроса. Статическая оболочка - навигация, лейаут, структура выше сгиба - приходит в первом чанке, потенциально за 50–100 мс. Динамические компоненты, зависящие от данных, приезжают следующими чанками по мере разрешения промисов, и каждый вставляется в нужное место DOM через инлайновые теги <script>, вызывающие внутреннюю функцию React $RC.
Наблюдаемое следствие для LCP: страница товара может отдать статическую оболочку первым чанком и получить FCP меньше 200 мс, пока динамика - отзывы, остатки, персональные рекомендации - приезжает следующими чанками Suspense, не блокируя элемент LCP. Архитектурно это отличается от классического SSR, где renderToString держит весь ответ, пока не разрешится каждый компонент дерева. Next.js превращает ровно этот паттерн статической оболочки с динамическими дырами в продакшен-режим рендеринга - смотрите частичный пререндеринг в продакшене.
Частичный пререндеринг: единая статико-динамическая модель
Partial Prerendering (PPR), стабилизированный в Next.js 15, распространяет стриминговую модель RSC на уровень CDN. PPR пререндерит статическую оболочку каждой страницы на сборке и кеширует её на краю сети. На каждый запрос CDN мгновенно отдаёт закешированную оболочку - ноль серверных вычислений, TTFB близкий к нулю, - а динамические дыры RSC, обёрнутые в границы <Suspense>, стримятся с origin-сервера. Браузер получает ответ логически из двух частей: закешированная структура и стримящаяся динамика, мультиплексированные по одному соединению HTTP/2.
PPR смотрит на дерево React как на граф, где узлы либо статически детерминированы (вывод зависит только от данных сборки), либо динамически зависимы (вывод зависит от данных запроса: заголовков, кук, состояния базы). Статические узлы пререндерятся и кешируются, динамические рендерятся на каждый запрос. Границей между ними служит компонент <Suspense>. Бенчмарк PPR от команды Vercel за 2025 год показывает снижение TTFB на 40–65% на страницах, которые прежде рендерились на сервере целиком.
Часть IV - Эмпирический эффект: размер бандла и бенчмарки
- Внутренняя миграция facebook.com (React Conf 2024): Перевод дерева компонентов отображения данных с клиентских на серверные сократил клиентский JavaScript для этого дерева на 78%. Около 120 КБ логики компонентов, кода запроса данных и утилит форматирования ушли из бандла. Передавался только отрисованный вывод - примерно 8 КБ нагрузки RSC.
- Shopify Hydrogen 2 (октябрь 2024): Перевод страницы товара с гибрида CSR и RSC преимущественно на RSC сократил JavaScript на страницу с 340 КБ до 89 КБ. TTI на симулированном Moto G Power улучшился с 4,2 с до 1,8 с. Источник: инженерный блог Shopify.
- Шаблон Vercel Commerce (январь 2025): Референсный eCommerce-шаблон, целиком построенный на RSC в App Router, отдаёт 67 КБ начального JavaScript против 210 КБ у эквивалентной реализации на Pages Router - минус 68%. Источник: блог Vercel.
- HTTP Archive 2025 Web Almanac: У сайтов на App Router медианный вес JavaScript-бандла 180 КБ против 390 КБ у сайтов на Pages Router с сопоставимой функциональностью. Разница в 210 КБ согласуется с теоретическим предсказанием о нулевом бандле для RSC.
Компилятор React 19: синергия с RSC и автомемоизация
Компилятор React 19 - преобразование на этапе сборки, которое автоматически вставляет мемоизацию, эквивалент useMemo, useCallback и React.memo, везде, где может доказать её безопасность и пользу. Работает он исключительно с клиентскими компонентами: у серверных вопроса мемоизации нет вовсе, потому что они на клиенте не перерендериваются.
Связка RSC и компилятора синергична: RSC сокращает объём кода, работающего на клиенте, выкидывая целые поддеревья из бандла, а компилятор сокращает частоту перерендеров того, что осталось. У хорошо выстроенного приложения на RSC 60–70% дерева обычно приходится на серверные компоненты с нулевой ценой перерендера и 30–40% на клиентские, оптимизированные компилятором. По бенчмаркам команды React (React Conf 2024) включение компилятора на продакшен-приложении, уже аккуратно отмемоизированном руками, дало ещё минус 22% перерендеров: ручная мемоизация даже у опытных инженеров оставляет заметный запас нереализованным.
Частые ошибки и архитектурные антипаттерны
- Злоупотребление
'use client': Самый распространённый антипаттерн. Директива на каждом компоненте фактически возвращает вас к модели Pages Router. Правильное значение по умолчанию: каждый компонент серверный, пока ему явно не понадобились состояние, эффекты или браузерные API. - Прокидывание пропсов через границу: Кладите запрос данных в самый глубокий серверный компонент, которому они нужны. RSC делает это дешёвым, потому что каждый асинхронный серверный компонент запрашивает данные независимо, а Next.js дедуплицирует запросы автоматически.
- Попытка мутировать через RSC: RSC - верный инструмент для чтения и рендера. Мутации остаются вотчиной Server Actions (
'use server'), React Query или SWR. - Большие сериализуемые пропсы: Передача каталога из 500 товаров JSON-пропсом из серверного компонента в клиентский отправит все 500 позиций в формате передачи RSC. Разбивайте на страницы или фильтруйте на сервере до пересечения границы.
- Отсутствие границ Suspense: Асинхронный серверный компонент без обёртки
<Suspense>заблокирует ответ всей страницы, пока не разрешится. Каждый такой компонент должен быть обёрнут в<Suspense fallback={<Skeleton />}>.
Фреймворк решения: серверный компонент или клиентский
- Использует
useState,useReducerилиuseContext? → Клиентский. У серверных компонентов нет состояния между рендерами. - Использует
useEffect,useLayoutEffectилиuseInsertionEffect? → Клиентский. Эффекты - колбэки жизненного цикла браузера. - Вешает обработчики событий DOM (
onClick,onChange,onSubmit)? → Клиентский. Обработчики - функции, а они через формат передачи RSC не сериализуются. - Обращается к браузерным API (
window,document,navigator,localStorage,IntersectionObserver)? → Клиентский. В серверной среде Node.js этих API нет. - Импортирует библиотеку, у которой что-то из перечисленного есть в графе модулей? → Клиентский, либо перестройте код так, чтобы вызов библиотеки уехал в листовой клиентский компонент.
- Ничего из перечисленного? → Серверный. Запрашивайте данные напрямую, рендерьте вывод, не добавляйте в клиентский бандл ни байта.
Целевая архитектура выглядит так: клиентские компоненты - листья дерева, минимально возможная граница вокруг интерактивного поведения, а серверные составляют структурное большинство.
Разбор: архитектура страницы товара в интернет-магазине
- Корень страницы (серверный, async): Запрашивает данные товара. Рисует статическую структуру - картинки, название, описание. Кешируется PPR на краю сети, потому что зависит только от
params.slug. <PriceBlock productId={id} />(серверный, async, в Suspense): Запрашивает персональную цену по идентификатору пользователя из кук. Стримится независимо. Ноль байт в клиентском бандле.<InventoryBadge productId={id} />(серверный, async, в Suspense): Запрашивает живые остатки. Стримится независимо от запроса цены.<AddToCartButton productId={id} price={price} />(клиентский): Держит состояние корзины черезuseState, по отправке вызывает Server Action. Единственная граница'use client'во всём дереве этой страницы.<RecommendationCarousel userId={userId} />(серверный, async, в Suspense): Тянет рекомендации модели на сервере и рисует статическую карусель.
Результат: страница товара отдаёт примерно 40–60 КБ клиентского JavaScript - кнопку «В корзину» и цену фреймворка - вместо 300–500 КБ, типичных для гибрида CSR и SSR. LCP приезжает первым чанком ответа. Персональные данные стримятся параллельными чанками Suspense, не блокируя LCP. INP на клике «В корзину» не зависит от сложности остальной страницы, потому что этих компонентов в клиентском бандле просто нет.
Server Actions: дополнение RSC для мутаций
Server Actions (директива 'use server' на функции) - это то, что дополняет модель RSC на запись. Такая функция исполняется на сервере, но вызывается из клиентского компонента как обычная функция JavaScript: фреймворк прозрачно берёт на себя сериализацию, транспорт (HTTP POST на сгенерированный эндпоинт) и цикл ответа.
Связка RSC и Server Actions даёт полностековую архитектуру компонентов, где и чтение, и запись выражаются примитивами React без вручную поддерживаемого слоя API. Вызовы revalidatePath() и revalidateTag() из Server Action запускают перерендер RSC на конкретных маршрутах после мутации, замыкая цикл чтения и записи без клиентского управления состоянием.
Заключение: RSC - смена парадигмы, а не оптимизация
RSC - не оптимизация производительности в привычном смысле. Она меняет вопрос с «как ускорить мой JavaScript» на «как сделать так, чтобы в браузер уезжал только тот JavaScript, которому там действительно место». Цифры это подтверждают: минус 68–78% бандла на продакшен-миграциях, TTI меньше двух секунд на среднем железе, минус 40–65% TTFB с PPR. Вместе с компилятором React 19 и частичным пререндерингом в Next.js 15 вы получаете характеристики, близкие к статическому HTML для большей части страницы, и полноценную интерактивность React только там, где она нужна.
Для ревью продакшен-архитектуры или стратегии миграции на App Router есть консалтинг по фронтенд-архитектуре и оптимизация производительности eCommerce - смотрите кейсы с реальными бенчмарками миграций или обсудите проект.
Источники
- Abramov, D., Dodds, K. и др. React RFC #188 - React Server Components (декабрь 2020)
- React Team. React 19 Release Notes (декабрь 2024)
- React Team. React Server Components - официальная документация
- Next.js Team. Документация архитектуры App Router
- Next.js Team. Документация Partial Prerendering
- Vercel Engineering. «Partial Prerendering: Making your app as fast as static HTML» (2025)
- Shopify Engineering. «Hydrogen 2: RSC Migration Benchmarks» (октябрь 2024)
- HTTP Archive 2025 Web Almanac, глава о JavaScript
- React Conf 2024 - разбор компилятора React (Sophie Alpert, Joe Savona)
- Osmani, A. «Patterns for Building React Applications» (2024, обновлено 2025)
- W3C React Working Group - RSC и веб-стандарты
