Серверные и клиентские компоненты: понятные правила для приложений на React
Разделение серверных и клиентских компонентов помогает сделать приложения на React быстрее и проще. Используйте понятные правила для загрузки данных, интерактивности и размера бандла.

Почему этот выбор становится сложным
Команды размывают границу, потому что большинство страниц одновременно решают две задачи. Они загружают данные и реагируют на действия людей. Страница товара может требовать цены с сервера, выбор размера в браузере, сохранённую корзину и форму отзыва. Если положить всё в один файл, граница начинает казаться необязательной.
Именно здесь обычно начинаются проблемы. Команда добавляет одну небольшую браузерную фичу — кнопку фильтра или переключатель локального состояния — и помечает весь компонент как клиентский. Это работает, и паттерн распространяется. Вскоре загрузка данных, код макета и статический контент переезжают в браузер тоже, хотя им этого и не требовалось.
Сдвиг часто начинается привычным образом. Родительский компонент становится клиентским из‑за того, что один ребёнок нуждается в обработчике клика. Загрузка данных остаётся рядом с UI‑состоянием, потому что один файл кажется проще. Общие компоненты переиспользуют в нескольких местах, и браузерная версия становится по умолчанию. Никто не замечает цену, пока страница не начнёт тяготить на реальном телефоне.
Одно плохое решение редко остаётся маленьким. В React решение распространяется вниз по дереву. Если высокоуровневый компонент становится клиентским, многие дети идут вслед за ним. Страница, которая была почти статичной, может начать отправлять пользователям гораздо больше JavaScript, чем нужно. Браузер должен скачать, распарсить и выполнить лишний код прежде, чем страница станет готова.
Эта цена не абстрактна. Дополнительный JavaScript может добавить секунду или две на медленных устройствах. Он может сделать простые страницы громоздкими, увеличить потребление памяти и породить состояния загрузки, которых можно было бы избежать. Команды начинают латать симптомы вместо того, чтобы исправлять границу.
Цель не в чистоте архитектуры. Цель — несколько правил, которым люди могут следовать без споров в каждом пулреквесте. Для большинства команд самый простой правило такое: по умолчанию держите загрузку данных и статическую отрисовку на сервере, а в браузер выносите только небольшие интерактивные части. Понятные правила чаще выигрывают у хитрой React‑архитектуры.
За что отвечают серверные компоненты
Серверные компоненты лучше подходят для частей страницы, которые в основном читают данные и показывают их. Если секции не нужны браузерное состояние, обработчики кликов или логика форм в реальном времени — оставьте их на сервере. Сетки товаров, страницы статей, сводки отчётов, обзор аккаунта и макеты результатов поиска обычно подходят.
Простой тест помогает. Если пользователь в основном смотрит на контент, сервер должен подготовить его. Браузер может сосредоточиться на меньших частях, с которыми действительно взаимодействуют люди.
Загрузка данных в React также чище на сервере. Приватные детали остаются там, где им место. Учётные данные базы данных, API‑токены, подписанные заголовки и внутренние вызовы сервисов не попадают в код браузера. Это снимает риск и избавляет от множества неудобных обходных решений.
Есть и выигрыш в производительности. Каждый клиентский компонент добавляет код, который браузеру нужно скачать, распарсить и выполнить. На быстром ноутбуке это может быть незаметно. На старом телефоне — часто медленно. Серверные компоненты сокращают эту цену, потому что отправляют отрендеренный результат вместо логики, которую браузер должен заново выполнять.
Это особенно хорошо работает для страниц с большим количеством контента, секций, которые тянут данные из приватных API или баз, и макетов, которые комбинируют данные из нескольких источников. Также это подходит для контента, который меняется редко: страницы с ценами, справочные статьи, журналы изменений, справочники команды или сводки дашбордов, которые обновляются раз в несколько минут. Сервер может отрендерить их по запросу или переиспользовать кэш, и страница сразу станет легче.
Пример — страница аналитики. Сервер может загрузить суммы использования, недавние события и данные биллинга, затем отрендерить общую сводку до того, как браузер сделает много работы. Если странице нужен выбор даты или панель фильтров, эти небольшие интерактивные части могут жить на клиенте, не превращая всю страницу в клиентский код.
Обычно это спокойный подход: тяжёлая часть для чтения на сервере, небольшие взаимодействия — в браузере.
За что отвечают клиентские компоненты
Клиентские компоненты должны делать то, что требует живого браузера и живого человека. Если кто‑то кликает, печатает, перетаскивает, открывает, закрывает или меняет что‑то на экране — эта часть должна быть на клиенте. Кнопка модального окна, переключение вкладок, поле поиска с мгновенной фильтрацией и выбор даты — всё это сюда.
Локальное состояние тоже сюда, но держите его близко к компоненту, который его использует. Текст ввода, выбранная опция, раскрытая строка или временный флаг загрузки не должны подниматься на уровень страницы без веской причины. Как только крошечное состояние выносят слишком высоко, большая часть дерева превращается в клиентский код без реальной пользы.
Хороший пример — подсказки в формах. Подсказки о силе пароля, предупреждения о формате email и счётчики символов должны сидеть рядом с полем ввода. Финальная проверка по‑прежнему должна выполняться на сервере, но клиент отвечает за мгновенную обратную связь, чтобы формы выглядели привычно.
Браузерные API очерчивают чёткую границу. Если код обращается к window, localStorage, navigator.clipboard, размеру экрана, управлению фокусом или позиции прокрутки — он должен выполняться в клиентском компоненте. Ошибка — превращать всю страницу в клиентскую из‑за одного виджета, который нуждается в localStorage.
Хороший дефолт — держать клиентскую поверхность как можно меньше. Размещайте оболочку страницы на сервере. Держите там также секции, насыщенные данными. Вынесите в клиент только интерактивную часть и передавайте туда простые props, когда это возможно.
Это помогает держать размер бандла React под контролем, не делая приложение тугим. Страница с ценами, например, может оставаться в основном серверной, а переключатель биллинга и контактная форма — работать на клиенте. Пользователи получают быстрый первый показ, а JavaScript отправляется только туда, где он действительно нужен.
Если компонент может отрисоваться один раз и оставаться статичным — оставьте его на сервере. Если ему нужно реагировать прямо сейчас — пусть клиент обрабатывает эту небольшую часть.
Правила для проведения границы
Большинство команд усложняют это излишне. Начинайте на сервере и переводите компонент в клиентский только тогда, когда можете объяснить, почему браузеру это необходимо. Серверный код может получать данные, читать куки, обращаться к базе и держать больше логики вне браузера. Это обычно означает более быстрый первый показ и меньший бандл.
Переводите компонент в клиентский только когда нужны взаимодействия в браузере: обработчики кликов, локальное состояние формы, drag and drop, живой ввод и всё, что зависит от браузерных API. Если компонент только читает данные и рендерит UI, держите его на сервере даже если разметки много.
Один быстрый тест работает хорошо: представьте компонент без кнопок, полей и анимаций. Если остаётся просто данные, превращённые в HTML — вероятно, ему место на сервере.
Когда данные пересекают границу, держите их простыми. Передавайте строки, числа, булевы, массивы и простые объекты. Не отправляйте экземпляры классов, клиенты базы данных, гигантские вложенные блоки или случайные функции‑хелперы. Простые props легче инспектировать, легче кэшировать и гораздо проще менять позже.
Обёртки должны быть тонкими и понятными. Клиентская обёртка может управлять одной маленькой частью состояния, например открытием модалки или переключением вкладок, а затем рендерить внутри себя серверный контент. Серверная обёртка может получить данные и выбрать, что показать. Ни одна из них не должна прятать половину приложения за слоями вспомогательных функций.
Короткий набор правил обычно работает:
- Начинайте каждый новый компонент на сервере.
- Переводите его в клиентский только для реального взаимодействия.
- Передавайте простые, маленькие props через границу.
- Держите обёртки короткими и читаемыми.
Поначалу это выглядит строгим. Через неделю‑две оно начинает экономить время. Команды перестают спорить о React‑архитектуре, потому что у каждой границы есть простая причина. Вы также быстрее замечаете лишнее, например всю страницу, помеченную как клиентскую ради одного выпадающего фильтра.
Как поэтапно выбирать загрузку данных
Начните с одного экрана и выпишите все данные, которые ему нужны. Будьте буквальны. Страница товара может требовать название, цену, наличие, отзывы и состояние корзины вошедшего пользователя. Как только вы записали данные, разделение становится проще.
Отметьте, что меняется из‑за действий пользователя. Если данные меняются после клика, ввода, перетаскивания или фильтра — эта часть, вероятно, требует клиентского компонента. Если их можно загрузить один раз и отрендерить как HTML — держите на сервере. Это обычно самый чистый ответ.
Простой процесс помогает:
- Перечислите данные для всего экрана, а не только для одного виджета.
- Отметьте части, которые меняются в реальном времени или после ввода пользователя.
- Получайте общие данные высоко в дереве, часто на уровне страницы или layout.
- Передавайте простые props вниз в маленькие интерактивные компоненты.
- Проверяйте стоимость JavaScript перед мерджем.
Загрузка общих данных высоко в дереве предотвращает повторные запросы и упрощает рассуждения о странице. Если хедер, сайдбар и основная панель все нуждаются в одних и тех же данных аккаунта — загрузите их один раз на сервере и передайте вниз. Не заставляйте три клиентских компонента запрашивать одно и то же только потому, что они расположены в разных местах.
Интерактивные острова должны оставаться низко в дереве. Поисковое поле, выпадающий список сортировки, чат‑инпут или кнопка сохранения могут жить в клиентском компоненте рядом с местом взаимодействия. Остальная часть страницы при этом остаётся серверной. Это уменьшает бандл и предотвращает отправку ненужного кода в браузер.
Проверяйте стоимость каждый раз. Одна маленькая директива use client не на том уровне может подтянуть в браузер большую часть дерева. Команды часто пропускают это на ревью потому, что фича работает. Страница всё ещё загружается, но пользователи платят более медленным стартом.
Хорошее правило для загрузки данных в React простое: получайте широкие, стабильные данные на сервере, а браузерный код фокусируйте на взаимодействии. Если компонент требует и того, и другого, разделите его на две части вместо того, чтобы заставлять один компонент делать всё.
Простой пример страницы
Представьте страницу товара — кроссовки для бега. Страница показывает название, цену, фото, наличие, полное описание и список отзывов. Также покупатель может выбрать количество и добавить товар в корзину.
Большая часть страницы должна рендериться на сервере. Детали товара и отзывы подходят, потому что берутся из базы или API и не требуют браузерного состояния, чтобы отображаться. Сервер может получить эти данные один раз, собрать HTML и отправить страницу, которая быстро загружается и хорошо индексируется поисковыми системами.
Остаётся гораздо меньшая работа для клиента. Селектор количества нуждается в локальном состоянии. Кнопке добавления в корзину нужны обработчики клика, индикатор загрузки и, возможно, короткое сообщение об успехе. Эти части должны быть в клиентском компоненте, потому что браузер должен сразу отреагировать на действия пользователя.
Разделение очевидно. Держите на сервере название, изображения, цену, описание и список отзывов. Перенесите в клиент селектор количества, кнопку добавления в корзину и обратную связь мини‑корзины.
Это уменьшает бандл. Отзывы и длинные описания могут быть объёмными. Если вы выносите всё это в клиентские компоненты, пользователи скачивают JavaScript для контента, который мог бы прийти как HTML. Это добавляет вес без выгоды.
Пользовательский опыт не ухудшается. Во многих случаях он становится лучше. Покупатели видят детали товара сразу, а интерактивные части остаются отзывчивыми, потому что они маленькие и сфокусированные.
Подумайте о медленном телефоне. Полностью клиентская страница товара может ждать загрузки JavaScript, прежде чем стать полезной. При хорошем разделении информация появляется первой, а в браузере гидрируется только код, который должен быть интерактивным.
Это правило работает для многих решений по загрузке данных в React. Если странице нужно отобразить полученный контент — начинайте на сервере. Если пользователь должен что‑то изменить кликами, вводом или локальным состоянием — выносите лишь этот фрагмент на клиент.
Ошибки, которые увеличивают бандл
Большинство проблем со серверными и клиентскими компонентами не начинается с гигантского приложения. Они начинаются с небольших ошибок границы, которые накапливаются. Страница сначала кажется нормальной, затем начинает отправлять больше JavaScript, чем нужно, повторно запрашивает одни и те же данные и превращает простой контент в работу для браузера.
Одна распространённая ошибка — пометить весь layout как клиентский из‑за того, что одна маленькая часть требует кликов. Хедер может содержать переключатель темы или поле поиска, но это не значит, что весь каркас страницы должен отправляться в браузер. Держите layout на сервере и вынесите интерактивную часть в маленькую клиентскую границу.
Команды также тратят мегабайты на повторные запросы. Серверный компонент загружает пользователя, затем клиентский виджет снова запрашивает того же пользователя после гидратации. Это добавляет сетевой трафик, лишнюю логику состояния и часто спиннер для данных, которые уже были доступны. Запрашивайте один раз и передавайте то, что действительно нужно виджету.
Большие props тихо вредят. Если интерактивному виджету нужен только ID товара, название и цена, не передавайте полный объект товара с отзывами, связанными товарами, историей инвентаря и полями CMS. Браузер должен получить и распарсить всё это. Маленькие props делают интерактивные компоненты дешевле.
Другой медленный паттерн — использование клиентского состояния для данных, которые почти не меняются. Политика доставки, таблица цен или сводка аккаунта часто лучше рендерятся на сервере. Когда вы кладёте стабильные данные в клиентское состояние, вы добавляете хуки, эффекты и работу гидратации без полезного результата для пользователя.
Библиотеки только для браузера могут распространять проблему по всему приложению. Пакет для графиков, редактор с богатым текстом или библиотека drag‑and‑drop должны жить в клиентской зоне. Если вы импортируете их в общий компонент по ошибке, страницы, которые даже не используют эту функциональность, могут заплатить за неё.
Простое правило поможет: отправляйте HTML для чтения, отправляйте JavaScript для действий. Если компонент в основном показывает контент — держите его на сервере. Если ему нужны браузерные API или мгновенный ввод — делайте эту часть клиентской и держите границу узкой.
Быстрая проверка перед деплоем
Страница обычно уходит в сторону в двух случаях: вы отправляете в браузер слишком много JavaScript или вы кладёте интерактивность на сервер и затем боретесь с UI. Короткое ревью перед релизом ловит оба.
Начните с самого компонента. Если пользователи никогда не кликают по нему, не вводят в него текст, не сортируют, не перетаскивают и не открывают/закрывают его — оставьте на сервере. Сводка товара, блок с деталями аккаунта или таблица только для чтения часто не нуждаются в клиентском коде.
Затем спросите, где должны быть данные. Если сервер может загрузить их один раз, отрендерить результат и передать простые props в браузер — сделайте так. Это уменьшает бандл и убирает логику загрузки с клиента. Оставьте клиентскую загрузку для случаев, когда пользователь меняет фильтры, редактирует данные или нужны живые обновления после первой отрисовки.
Ещё одна распространённая ошибка скрывается на уровень выше. Один клиентский родитель может подтянуть в браузер большой поддерево, даже когда большинство детей статичны. Это случается с обёртками layout, вкладочными шеллами и контейнерами страницы. Если только один маленький виджет нуждается в состоянии — опустите клиентскую границу вниз и держите остальное на сервере.
Используйте этот чеклист перед мерджем:
- Проверьте, держит ли компонент обработчики кликов, вводы, открытие/закрытие или локальное UI‑состояние.
- Проверьте, может ли сервер получить данные один раз и передать результат как props.
- Проверьте, не тянет ли клиентский родитель серверно‑дружественных детей в браузерный код.
- Проверьте отчёт по бандлу после изменения, а не только поведение UI.
Последний шаг важен. Хорошие догадки всё равно могут провалиться. Небольшой рефактор может добавить библиотеку для графиков, выбор даты или клиентскую обёртку, которая стоит значительно дороже, чем вы ожидали.
Полезная привычка — сравнивать размер бандла и стоимость перезагрузки после каждого изменения. Если одна кнопка добавила 80 КБ лишнего JavaScript, вынесите эту кнопку в меньший клиентский остров. Один такой ход часто держит страницу быстрой, не меняя восприятия пользователем.
Что делать дальше
Не пытайтесь исправить всё приложение за один раз. Выберите одно правило, которое команда сможет применить уже на этой неделе, и запишите его там, где его будут видеть при ревью.
Хорошее стартовое правило простое: если компонент не требует кликов, ввода, локального состояния или браузерных API — держите его на сервере. Это решит множество споров ещё до их начала.
Затем выберите одну страницу, которая уже кажется тяжелее, чем должна быть. Страница настроек, отчётности или карточка товара обычно подходят. Сначала верните на сервер неинтерактивные части: заголовки, сводки, таблицы только для чтения и блоки контента. Оставьте на клиенте только то, что действительно реагирует на ввод.
Делайте первый проход небольшим и измеримым. Запишите правило в шаблоне PR или инженерных заметках. Выберите одну страницу и переместите пару спокойных, только для чтения компонентов на сервер. Сравните размер бандла, время загрузки и работу браузера до и после. Если страница стала легче и код проще объяснить — оставьте изменение.
Цифры важны. Если страница отправляет меньше JavaScript и стартует быстрее на среднем ноутбуке или телефоне — это победа. Если команда не может объяснить, почему компонент на клиенте — скорее всего, его стоит вернуть.
Небольшой пример показывает суть. Допустим, дашборд имеет статичную сводку аккаунта, фильтр для графика и кнопку экспорта. Сводка должна остаться на сервере. Фильтр и кнопка экспорта — на клиенте. Такое разделение проще поддерживать и обычно уменьшает размер бандла с минимальным риском.
Если команда постоянно вертится вокруг одних и тех же вопросов, внешнее архитектурное ревью может помочь. Oleg Sotnikov на oleg.is работает как Fractional CTO и стартап‑советник с глубоким опытом в продуктовой архитектуре, lean‑инфраструктуре и AI‑ориентированной разработке. Небольшой аудит от человека с таким опытом может помочь установить простые правила и прекратить повторные споры о решениях в React.
Одна страница — достаточно, чтобы начать. После этого паттерн обычно становится очевиден.
Часто задаваемые вопросы
С чего начать: серверный или клиентский компонент?
Начинайте на сервере. Переводите компонент в клиентский только тогда, когда браузер должен обрабатывать клики, ввод, локальное состояние или браузерные API. Такое правило по умолчанию делает страницы легче и сокращает споры при ревью.
Что должно быть в серверном компоненте?
Используйте серверный компонент для контента, который люди преимущественно читают. Детали товара, страницы статей, сводки, таблицы только для чтения и макеты, которые получают данные — обычно подходят, потому что их можно отрендерить один раз и оставить неизменными.
Что должно быть в клиентском компоненте?
Поместите интерактивность на клиент. Кнопки, поля ввода, вкладки, модальные окна, drag-and-drop и код, который обращается к window или localStorage, должны быть на клиенте. Поддерживайте эту область маленькой, чтобы остальная часть страницы оставалась дешёвой.
Как поступать со страницей, которая требует и загрузки данных, и взаимодействия пользователя?
Разделите страницу на две задачи. Сервер получает и рендерит основной контент, а интерактивный виджет оборачивается в клиентский компонент — ему передаются простые props.
Почему один клиентский родитель делает страницу тяжелее?
Потому что решение распространяется вниз по дереву. Если пометить высокоуровневый родитель как клиентский, многие дочерние элементы тоже попадут в бандл браузера, даже если они лишь отображают статический контент. Пользователи в итоге скачивают и выполняют больше JavaScript без пользы.
Стоит ли снова запрашивать те же данные на клиенте после гидратации?
Обычно — нет. Если сервер уже имеет данные, загрузите их там один раз и передайте виджету только то, что ему нужно. Клиентская загрузка оправдана, когда пользователь меняет фильтры, редактирует данные или нужны живые обновления после первой отрисовки.
Какие props стоит передавать с сервера на клиент?
Передавайте простые данные: строки, числа, булевы значения, массивы и небольшие объекты. Избегайте гигантских вложенных структур, экземпляров классов, клиентов базы данных и вспомогательных функций, когда виджету нужны лишь несколько полей.
Как понять, когда компонент стоит вернуть на сервер?
Задайте прямой вопрос: если я удалю все кнопки, поля и анимации, останется ли что‑то интерактивное? Если нет — верните компонент на сервер.
Какое разделение для страницы товара будет хорошим?
Рендерьте на сервере заголовок, изображения, цену, описание, наличие на складе и отзывы. Держите выбор количества, кнопку «добавить в корзину» и обратную связь по корзине на клиенте. Покупатели сразу увидят контент, а вы отправите меньше кода.
Когда команде стоит попросить архитектурного ревью?
Привлекайте внешнюю экспертизу, когда те же споры о границах постоянно тормозят ревью, вес страниц продолжает расти или никто не может объяснить, почему компонент находится на клиенте. Опытный CTO, например Oleg Sotnikov (oleg.is), может помочь установить простые правила и устранить самые крупные ошибки.