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 і вебстандарти
