React Server Components: Архитектура, лежащая в основе отображения без формирования пакетов.
Понять логику создания React Server Components, начиная с проблем размера пакетов и последовательной обработки данных, заканчивая границей между сервером и клиентом, а также содержимым данных RSC.
Если вы потратили время на попытки разобраться в React Server Components и в итоге остались ещё более сбитыми с толку, чем в начале, это не означает, что у вас отсутствует какая-то очевидная информация. Это значит, что объяснения, которые вы нашли, сами были частью проблемы. В течение нескольких лет официальное описание Server Components звучало как «компоненты, которые отрисовываются на сервере», что странно напоминает функции getServerSideProps или традиционную обработку серверной отрисовки в Next.js. Затем появилось утверждение, что эти компоненты «не отправляют никакого JavaScript на клиент», что скорее звучит как трюк для улучшения производительности, чем как новый способ структурирования приложения. После появления App Router вдруг каждый файл в проекте Next.js автоматически стал Server Componentом, если только вы не добавили специальную строку в начало файла.
Эта путаница не была вашей ошибкой. Она возникла потому, что действительно новую парадигму описывали с использованием лексики, заимствованной из более старой. Этот руководство призвано устранить этот разрыв. К моменту окончания чтения вы должны понять не только механизмы Server Components, но и причины, по которым они представляют собой совершенно иной подход к разработке React-приложений.
Проблема, которую решают RSC
Прежде чем углубляться в то, как функционируют Server Components, полезно понять, что изначально побудило React их создать. Проблема никогда не заключалась в слишком медленной работе серверного рендеринга. Настоящая проблема заключалась в том, что модель компонентов React была построена на предположении, что всё происходит в браузере, даже в тех случаях, когда это предположение приводило к ненужным затратам.
Ловушка размера пакета
In the classic React model, every component you author turns into JavaScript that gets shipped to the browser, no exceptions. It makes no difference whether that component just renders some static markdown, does a simple date calculation, or hits a database. As long as it lives somewhere in your component tree, its code lives in your bundle too.
That creates an unforgiving trade-off. Adding more components inflates your JavaScript payload. A heavier payload pushes out your Time to Interactive. In practice, you end up delivering code to browsers that never had any reason to execute it.
The Waterfall Problem
До появления Server Components любой компонент, нуждавшийся в данных, должен был получать их после того, как они достигали клиента. Типичным подходом было отображение временной оболочки, запуск функции useEffect, ожидание ответа и только затем отображение реального содержимого. Если это содержимое включало вложенный компонент с собственными потребностями в данных, цикл повторялся: ещё одна функция эффекта, ещё одно ожидание, ещё одна задержка, накладывающаяся на предыдущую. Эта цепная реакция и называется водопадным подходом к получению данных, и она объясняет, почему многие приложения на React кажутся медленными, даже если их бэкенд-API отвечают быстро.
Одним из решений было перемещение процесса загрузки данных на уровень маршрута с использованием таких инструментов, как getServerSideProps или загрузчики маршрутов, но это имело свою цену: это нарушало принцип самодостаточности компонентов. Теперь странице приходилось знать о данных, которые по сути принадлежали её дочерним компонентам, и компоненты переставали быть независимыми единицами.
Плата за гидратацию
Традиционная рендеринг с серверной стороны работает путем генерации HTML на сервере, его отправки и последующей перерендеринга всего приложения с использованием JavaScript на клиенте для обеспечения интерактивности. Это означает, что каждый компонент, включая те, которым никогда не понадобится клиентская логика, всё равно должен пройти процедуру гидратации. По сути, вы платите за интерактивность даже для полностью статического контента.
React Server Components решают все три этих проблемы на уровне архитектуры, а не как временное улучшение производительности.
Основная идея: Server Components — это не «SSR 2.0»
Вот основная идея, на которой строится весь этот гид: Server Components — это не способ отрисовки страниц. Это отдельная категория компонентов.
В классическом React существовал только один тип компонентов. Они выполнялись в браузере, могли хранить состояние, запускать эффекты и реагировать на события. Весь их жизненный цикл, от создания до уничтожения, происходил на стороне клиента.
Компоненты сервера React добавляют вторую категорию. Компонент сервера выполняется на сервере. Он может свободно работать с файловой системой, напрямую запрашивать данные из базы данных или использовать библиотеки, которые функционируют только внутри Node.js. Однако он не может использовать useState, useEffect или привязывать обработчики событий, поскольку он вообще не запускается в браузере. Для него нет этапа гидратации. Его код даже не передается клиенту в виде JavaScript.
Полезный способ это представить: дерево компонентов React теперь является комбинированным. Некоторые ветви генерируются на сервере, другие — в браузере. Ветви с серверной стороны отрисовываются ровно один раз во время запроса, а результат их работы передается клиенту в виде сериализованных элементов React. Ветви с клиентской стороны отрисовываются в браузере как обычно и сохраняют все привычные интерактивные возможности.
Это отличный механизм от SSR. При серверной рендеринге вся приложение преобразуется на сервере в HTML, а затем полностью загружается обратно в браузер. В отличие от этого, RSC рендерит только определённые компоненты на сервере и вообще не передаёт их код в браузер.
Что на самом деле отправляется клиенту
Когда серверный компонент выполняется, он не выводит HTML. Вместо этого он генерирует сериализованный описатель своего вывода, называемый нагрузкой RSC — поток инструкций в формате JSON, который указывает React на стороне клиента, как собрать дерево элементов, необходимое для отображения.
Вот последовательность событий, происходящих на фоне при запросе страницы с серверными компонентами:
- React рендерит серверные компоненты на сервере.
Ключевой вывод заключается в том, что код серверных компонентов никогда не попадает в пакет JavaScript, отправляемый в браузеры. Представьте компонент MarkdownRenderer, построенный на библиотеке для парсинга Markdown объемом 200 КБ. Если этот компонент является серверным, вся эта библиотека остается на сервере. Браузер видит только структуру, сгенерированную парсером, но никогда сам парсер.
В этом суть утверждения о нулевом размере пакета, и это не просто незначительная техника оптимизации. Это реальное изменение в том, что на самом деле подразумевается при создании React-приложения.
Граница между сервером и клиентом
Внутри App Router каждый файл считается серверным компонентом, если не указано иное. Это не просто стилистический стандарт — это структурное предположение, встроенное в фреймворк, отражающее идею о том, что большая часть интерфейса вообще не обязана выполняться в браузере.
Клиентский компонент, напротив, — это код, предназначенный для выполнения на компьютере пользователя. Чтобы пометить файл как клиентский, необходимо разместить в его верхней части директиву "use client". Это не просто рекомендательный текст — это жесткое ограничение. Она указывает компилятору React на то, что всё, что определено в этом файле, а также всё, что импортируется из него, должно быть упаковано и доставлено в браузер.
Правило, которое обеспечивает целостность всей этой системы, заключается в следующем: компоненты сервера могут свободно импортировать и отображать компоненты клиента, но обратное никогда не допускается — компонент клиента не может импортировать компонент сервера.
На первый взгляд это кажется нелогичным. Разве код серверной части не должен быть доступен из любого места, включая файлы клиента? На самом деле это не так, если рассмотреть, как на самом деле передаются данные:
- Сначала выполняется компонент сервера, у которого есть прямой доступ к базе данных и ресурсам серверной части. Он загружает необходимые данные и формирует структуру интерфейса. В этой структуре он может отобразить что-то вроде
<UserProfile />, для чего требуется интерактивность — поэтому такая часть пишется в виде компонента клиента. Родительский компонент сервера передает полученные данные пользователя в виде атрибутов.
Вот почему подобная схема не может сработать:
// ❌ Impossible: Client Component importing Server Component
'use client';
import { ServerDataFetcher } from './ServerDataFetcher'; // This breaks!
export function ClientWidget() {
return (
<div>
<ServerDataFetcher /> {/* Cannot render a server component here */}
</div>
);
}
Но структурирование в обратном порядке совершенно допустимо:
// ✅ Correct: Server Component importing Client Component
// This is a Server Component (no 'use client')
import { ClientWidget } from './ClientWidget';
async function ServerDataFetcher() {
const data = await db.query('SELECT * FROM posts');
return (
<div>
<h1>Latest Posts</h1>
{data.map(post => (
<ClientWidget key={post.id} post={post} />
))}
</div>
);
}
Паттерн прост: серверный компонент отвечает за получение данных и структурное отображение, затем передаёт данные интерактивным компонентам, находящимся в концах дерева. Эти листовые компоненты обрабатывают клики, отправку форм и анимации. Результатом является архитектура, по сути представляющая собой дерево, отрисованное на сервере, с участками интерактивности, расположенными на его краях.
Асинхронные компоненты: революция в получении данных
В классическом React функции компонентов никогда не могли быть асинхронными. Прямое использование оператора await внутри тела компонента не было возможным — приходилось вручную использовать хук useEffect и управлять состоянием загрузки.
Компоненты серверной части устраняют это ограничение: они могут быть async. Это кажется незначительным синтаксическим дополнением, но оно существенно меняет основную архитектуру.
// ✅ Server Component: Direct data access, no useEffect
async function BlogPostList() {
// This runs on the server. No fetch call in the browser.
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
);
}
Обратите внимание на всё, чего здесь нет. Нет хука useState для отслеживания флага загрузки, нет хука useEffect для запуска запроса, нет вручную создаваемого экрана-скелета. Компонент просто преобразует записи из базы данных непосредственно в элементы React. Теперь запросы выполняются для каждого компонента отдельно, а не для каждого маршрута, причем полностью на стороне сервера, никогда не в браузере.
Благодаря этому проблема водопадного эффекта исчезает — сервер может одновременно обработать все асинхронные компоненты, прежде чем отправить ответ клиенту. Запросы к базе данных выполняются в том же центре обработки данных, что и ваш бэкенд, поэтому задержка измеряется в микросекундах, а не в миллисекундах, характерных для передачи данных через мобильное соединение.
Директива “use client”: когда и почему
Добавление "use client" помечает файл как принадлежащий среде выполнения браузера. Она необходима каждый раз, когда компонент взаимодействует с чем-то, что по своей природе является клиентской частью:
- Локальное состояние с помощью
useStateилиuseReducer - Хуки жизненного цикла, такие как
useEffectилиuseLayoutEffect - Обработка событий, например
onClickилиonSubmit
localStorage, window, documentuseRouter или таких инструментов, как useMediaQueryРаспространённой ошибкой является размещение этого директивы слишком высоко в иерархии компонентов. Разработчики часто преобразуют всю страницу в клиентский компонент лишь потому, что одна из встроенных кнопок требует интерактивности.
// ❌ Bad: Making the whole page client-side for one interactive element
'use client';
import { useState } from 'react';
import { HeroSection } from './HeroSection'; // Static, could be server
import { LikeButton } from './LikeButton'; // Interactive, needs client
export default function Page() {
return (
<div>
<HeroSection />
<LikeButton />
</div>
);
}
В этом фрагменте HeroSection представляет собой чисто статический маркап, который можно было бы легко оставить для серверной обработки. Однако поскольку "use client" было объявлено на уровне родителя, все импорты ниже него — включая этот статический раздел — тем не менее пакетируются и отправляются в браузер.
Решение: переместить "use client" ниже в иерархии
Решение заключается в том, чтобы сама страница оставалась серверным компонентом, а интерактивная часть рассматривалась как листовой узел, импортируемый в неё.
// ✅ Good: Page is server, only LikeButton is client
// page.tsx (Server Component by default)
import { HeroSection } from './HeroSection';
import { LikeButton } from './LikeButton';
export default function Page() {
return (
<div>
<HeroSection />
<LikeButton postId="123" />
</div>
);
}
// LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
При такой структуре ни HeroSection, ни Page не отправляются в браузер в виде JavaScript. Только LikeButton вместе с вызовом useState попадает туда. Это и есть практический механизм, обеспечивающий цель нулевого размера пакета: как можно больше элементов дерева компонентов остается на сервере, а клиентская часть предназначена только для тех узлов, которым действительно нужна интерактивность.
Переплетение: принцип, делающий RSC мощным
Техника, которая придает RSC настоящую силу, — это перекрестная интеграция: вложение серверных компонентов внутрь клиентских компонентов, передача данных, полученных с сервера, в качестве параметров, и предоставление клиентским компонентам возможности использовать слоты children, в которых могут находиться дополнительные серверные компоненты.
// Layout.tsx (Server Component)
import { Sidebar } from './Sidebar';
import { AnalyticsProvider } from './AnalyticsProvider';
export default async function DashboardLayout({ children }) {
// Fetch user data on the server
const user = await getCurrentUser();
const permissions = await getUserPermissions(user.id);
return (
<div className="dashboard">
<Sidebar user={user} permissions={permissions} />
{/* AnalyticsProvider is a Client Component */}
<AnalyticsProvider userId={user.id}>
{/* children here can be a Server Component page */}
<main>{children}</main>
</AnalyticsProvider>
</div>
);
}
// AnalyticsProvider.tsx
'use client';
import { createContext, useContext } from 'react';
const AnalyticsContext = createContext(null);
export function AnalyticsProvider({ userId, children }) {
// Client-side analytics initialization
useEffect(() => {
analytics.identify(userId);
}, [userId]);
return (
<AnalyticsContext.Provider value={{ userId }}>
{children}
</AnalyticsContext.Provider>
);
}
Здесь DashboardLayout работает на сервере и напрямую запрашивает данные из базы. Он передает эти данные в Sidebar, который сам по себе может быть серверным или клиентским компонентом в зависимости от его функциональных требований. Кроме того, весь остальной контент страницы оборачивается компонентом AnalyticsProvider, который является клиентским компонентом и необходим в браузере для запуска сторонней библиотеки аналитики.
Важным элементом является свойство children. То, что отображается внутри AnalyticsProvider, не обязательно превращается в код клиента только потому, что оно находится там. React передаёт уже отрендеренный вывод серверного компонента через клиентский компонент в виде children — клиентский компонент служит лишь обёрткой или границей, а не преобразователем, заставляющим всё, что находится внутри него, выполняться с клиентской стороны.
Именно так строятся гибридные интерфейсы: контент, отрендеренный на сервере, размещается внутри обёрток, доступных только с клиентской стороны, именно там, где действительно требуется интерактивность.
Полезная нагрузка RSC: взгляд под капот
Отрендерив серверный компонент, React не генерирует HTML напрямую. Вместо этого он отправляет полезную нагрузку RSC — двоичный поток, который концептуально выглядит примерно так:
1:I["node_modules/react/jsx-runtime.js", "jsx"]
2:I["./components/ClientWidget.js", "default"]
0:["quot;, "div", null, {"children": [
["quot;, "h1", null, {"children": "Latest Posts"}],
["quot;, "@2", null, {"postId": "123", "title": "Hello World"}]
]}]
Каждая строка в этом потоке представляет собой инструкцию. Символ $ обозначает элемент React. I указывает на импорт модуля клиентского компонента. Ссылка вида @2 означает обращение ко второму импорту — в данном случае к ClientWidget. Обратите внимание, что сервер уже полностью отрендерил тег h1, но для ClientWidget он лишь передал атрибуты и указал местоположение его кода, не отрендерив его самого.
Как только браузер получает этот поток, он разрешает соответствующие ссылки на импорты, загружает необходимые пакеты клиентских компонентов и формирует окончательную структуру дерева компонентов. Поскольку данные поступают поэтапно, браузеру не нужно ждать их полной загрузки перед началом отрисовки. Именно такая поэтапная доставка позволяет технологии RSC обеспечивать потоковую отрисовку на сервере, избегая при этом типичных проблем с процессом гидратации.
Кэширование и повторная верификация
Next.js 15 и новее предоставляют серверным компонентам доступ ко всему спектру стратегий кэширования:
- Статическая отрисовка (по умолчанию). Если компонент не читает динамические данные и не выполняет запросы без кэширования, он отрисовывается статически во время сборки.
// Cached indefinitely at build time
async function ProductList() {
const products = await fetch('https://api.example.com/products');
// ...
}
- Динамическая отрисовка. Вызов функций
cookies(),headers()или чтение значений изsearchParams, а также явное установление значенияexport const dynamic = 'force-dynamic'заставляют компонент отрисовываться при каждом входящем запросе. - Повторная проверка данных по таймеру.
// Revalidate every 60 seconds
async function ProductList() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
});
// ...
}
- Повторная проверка данных по требованию.
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST() {
revalidatePath('/products');
return Response.json({ revalidated: true });
}
Главный вывод заключается в том, что поведение кэша теперь связано с отдельными компонентами, а не со всеми маршрутами. Два компонента на одной странице могут соблюдать совершенно разные правила кэширования. Такая степень детализации не имеет аналогов в классическом SSR, где один заголовок кэша управляет всей страницей сразу.
Устойчивые недопонимания
«Компоненты сервера существуют в основном ради SEO». Не совсем — улучшение индексации поиска является побочным эффектом, а не целью. Настоящая причина заключается в сокращении объема JavaScript на стороне клиента и возможности загрузки данных на уровне компонентов.
«Любой компонент, использующий хук, требует использования use client.» Это верно только в том случае, если хук действительно зависит от API браузера. Хук use в React 19 позволяет разворачивать обещания и читать контекст непосредственно внутри компонентов сервера, поэтому многие хуки работают без какого-либо взаимодействия со стороной клиента.
«Компоненты сервера делают маршруты API устаревшими». Это не так — они работают вместе. Компоненты сервера берут на себя задачу загрузки данных при первоначальной отрисовке, но вам все равно нужны маршруты API для обработки изменений, входящих webhook-сообщений от третьих сторон и любой загрузки данных, происходящей на стороне клиента после гидратации.
«У серверных компонентов нет состояния». У них отсутствует состояние в стиле браузера — нет функции useState — но они могут свободно обращаться к источникам состояния на стороне сервера, таким как базы данных, кэши, файловая система и переменные окружения.
«Контекст запрещен в серверных компонентах». Вы действительно не можете использовать React Context внутри серверного компонента, поскольку контекст существует именно для того, чтобы избежать передачи свойств по вертикальной иерархии в компонентах, отрендерившихся на клиенте. Вместо этого можно передавать данные в виде обычных свойств, и поскольку серверная отрисовка синхронно решает иерархию компонентов сверху вниз, Next.js также предлагает такие возможности, как unstable_rootParams, наряду с обычной передачей свойств по иерархии.
Ситуация на 2026 год
Теперь, когда React 19 достиг стабильности, сопутствующие инструменты значительно созрели:
- Функции серверных действий позволяют компонентам клиента вызывать асинхронные функции, которые выполняются непосредственно на сервере, тем самым сглаживая границу между клиентом и сервером при внесении изменений.
- Хук
useпредоставляет компонентам клиента способ разбирать обещания и контекст без необходимости использованияuseEffect. - Частичная предварительная отрисовка (PPR) в Next.js 15 позволяет подавать статическую оболочку непосредственно из CDN, в то время как динамические разделы загружаются с основного сервера.
- Компилятор React автоматически обрабатывает мемоизацию компонентов клиента, сокращая необходимость в ручных вызовах
useMemoи делая переход между клиентом и сервером более плавным.
Концептуальная модель на данный момент стабилизировалась. Компоненты сервера больше не являются опцией, которую можно включить — они представляют собой основополагающее условие построения современных приложений на React. Теперь исключением становятся компоненты клиента: специально предусмотренный выходной механизм для интерактивного слоя.
Изменение архитектуры, а не только производительности
Компоненты сервера React — это не просто улучшение производительности. Они означают переосмысление того, где на самом деле выполняется код React. Примерно десять лет React был по сути библиотекой для браузера, которую принудительно адаптировали для работы на стороне сервера в основном ради удовлетворения требований SEO. Сегодня это что-то другое: полноценная система компонентов, работающая на обоих концах сетевого соединения, при этом у каждой части четко определены границы, обязанности и характеристики производительности.
Сервер больше не является просто местом для получения данных — теперь это настоящая среда отрисовки. А браузер уже не единственное место для компонентов React; на самом деле именно здесь реализуется интерактивность.
Как только становится ясна эта концепция, многие недопонимания относительно RSC исчезают. Вопрос меняется с «Мне нужен ли здесь use client?» на «Куда должна относиться эта конкретная часть логики?» Именно этот вопрос стоит задавать, и именно такое мышление помогает развиваться приложениям по мере их роста.
Связанные материалы
- 20 передовых паттернов Next.js 16 для архитектуры приложений высокого уровня — обзор подходов серверного первенства, кэширования, стриминга, технологии PPR, параллельных и перехватывающих маршрутов, а также других паттернов для создания масштабируемых приложений на Next.js 16.
- Фронтенд в 2027 году: серверное первенство, TypeScript и стандарты Edge — подробный анализ того, как фреймворки с серверным первенством, обязательное использование TypeScript, кодирование с помощью ИИ и рендеринг на узлах Edge преобразуют практики разработки фронтенда.