open atlas
↑ К треку
React с нуля до senior RCT · 11 · 01

Модель RSC: две среды, одно дерево

Серверные компоненты выполняются раз на запрос — async, прямой доступ к данным, ноль веса в бандле. Клиентские гидрируются и живут. Flight-пейлоад — сериализованное дерево элементов с дырками-ссылками на модули, не HTML, и он компонуется с SSR, а не заменяет его.

RCT Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Платформа документации была классическим SPA: каждая страница статьи тянула markdown-парсер, подсветку синтаксиса с файлами грамматик, санитайзер и библиотеку дат с локалями — 1,1 МБ минифицированного JavaScript ради текста, который после публикации не менялся. Переписали на React Server Components: те же библиотеки теперь работают на сервере, клиентский бандл маршрута статьи упал примерно до 210 кБ, а total blocking time на среднем телефоне — с 2,1 с до менее 400 мс. Через две недели коллега добавил кнопку Copy внутрь компонента статьи — onClick={copy} — и dev упал с ошибкой «Event handlers cannot be passed to Client Component props». Тред в PR заполнился искренним недоумением: это же React-компонент, почему ему нельзя onClick? Потому что это серверный компонент: он выполнился один раз, на сервере, пока собирался ответ. К моменту, когда человек сможет куда-то кликнуть, функции за этим onClick — её замыкания, её модуля, состояния её процесса — может уже не существовать. Понимать, какие части дерева где живут и чему разрешено путешествовать между ними, — это и есть вся модель RSC.

К концу урока ты будешь точно знать, почему тот onClick упал — и как это починить, не раздув бандл.

Две среды, одно дерево

React-дерево в RSC-фреймворке растянуто между двумя средами. Серверные компоненты — поведение по умолчанию, без директивы — выполняются на сервере, один раз на запрос (или раз на ревалидацию, если фреймворк кэширует). Они могут быть async-функциями: await db.query(...), чтение файловой системы, вызов внутреннего сервиса — доступ к данным без API-эндпоинта посередине. Поскольку они отрабатывают до конца и исчезают, им недоступны useState, useEffect, обработчики событий и window — для них не существует никакого «потом» ни в одном браузере. Клиентские компоненты — модули, начинающиеся с директивы 'use client', — собираются в бандл, скачиваются, гидрируются и затем живут: состояние, эффекты, обработчики, браузерные API. Одно дерево свободно перемежает оба вида: серверная страница рендерит клиентский RangePicker, который сам может получать отрендеренные на сервере children (это переплетение — тема следующего урока).

Одну терминологическую ловушку стоит убить сразу: «серверный компонент» не означает «рендерится на сервере» — SSR уже десять лет рендерит на сервере клиентские компоненты. Это означает: существует только на сервере. Код такого компонента никогда не попадает ни в один бандл и никогда не выполняется в браузере повторно.

Что покупает серверная половина дерева

Три конкретных выигрыша, по убыванию того, как часто они окупают миграцию:

  • Ноль веса в бандле за серверный код. Подсветка синтаксиса с грамматиками и темами — сотни кБ даже в минифицированном виде; markdown-парсер плюс санитайзер — ещё десятки; стек дат/i18n с данными локалей — столько же. В серверном компоненте эти импорты не стоят клиенту ничего — платформа из Hook срезала так ~900 кБ, не удалив ни одной фичи. Пользователь скачивает результат работы библиотек, а не сами библиотеки.
  • Прямой доступ к данным. Запрос живёт рядом с компонентом, которому нужен, выполняется на латентности датацентра (доли миллисекунды — единицы мс до базы против 50–300 мс RTT из браузера) и не открывает API-поверхность, которую пришлось бы отдельно версионировать и защищать.
  • Автоматический код-сплиттинг. Каждая граница 'use client' — точка разреза: клиентские компоненты приезжают отдельными чанками, на которые ссылается пейлоад, и грузятся, когда дерево их действительно монтирует. Никто не ведёт бухгалтерию React.lazy вручную.

Вместе эти три свойства означают: серверная половина дерева окупается структурно — тяжёлые библиотеки не достигают клиента, получение данных перемещается туда, где оно дешевле всего, а код-сплиттинг становится результатом архитектуры, а не ручного труда. Отказаться от модели серверных компонентов полностью — значит вернуть все три в обмен на более простую ментальную модель: настоящий компромисс, не бесплатный обед.

Flight-пейлоад: дерево с дырками, а не HTML

Когда приходит запрос, серверные компоненты выполняются и растворяются в своём выводе. По проводу уходит RSC-пейлоад (формат, который React внутри называет Flight): сериализованное дерево элементов, а не разметка. Форма упрощена:

0:["$","article",null,{"children":[
  ["$","h1",null,{"children":"Q3 traffic report"}],
  ["$","$L1",null,{"initialRange":"30d","points":[120,84,97]}]]}]
1:I["./RangePicker.js",["chunk-1f9a"],"RangePicker"]

Строка 0 — уже разрешённая разметка, которую произвели серверные компоненты: обычные элементы, работа сделана. $L1дырка: ссылка на модуль, куда монтируется клиентский компонент. Строка 1 её разрешает — какой чанк скачать, какой экспорт использовать, — а пропсы в дырке (initialRange, points) были сериализованы на сервере, поэтому они вообще обязаны быть сериализуемыми. Заметьте, чего здесь нет: самих серверных компонентов, их кода загрузки данных, markdown-парсера. Они внесли вывод, а не код. Пейлоад к тому же стримится — строки приходят постепенно, и медленный запрос в глубине дерева не блокирует строки выше.

Викторина

При клиентской навигации в RSC-фреймворке тело ответа — не HTML. Что это, и зачем клиентскому рантайму I-строки (ссылки на модули)?

RSC и SSR — разные слои, и они компонуются

Это различие гоняют на интервью, потому что продакшен-команды его упорно смешивают. SSR берёт клиентские компоненты и рендерит их в HTML один раз на сервере, чтобы пользователь увидел пиксели до загрузки бандла; код всё равно уезжает, гидрация всё равно выполняется, и дальше компонент живёт в браузере. SSR — оптимизация первой отрисовки для клиентского React. RSC — постоянный серверный слой: серверные компоненты перерендериваются на сервере при каждой навигации или рефетче, а их код не уезжает вовсе.

Они компонуются, а не конкурируют. Первая загрузка: серверные компоненты выполняются → Flight-пейлоад → SSR-проход рендерит всё дерево (включая клиентские компоненты) в HTML → браузер сразу рисует → гидрация оживляет только клиентские компоненты, используя встроенный пейлоад как описание дерева. Дальнейшая навигация: никакого HTML — клиент забирает свежий Flight-пейлоад нового маршрута, и React реконсилирует его в живой DOM, сохраняя состояние клиентских компонентов, оставшихся на месте. HTML появляется в этом конвейере ровно один раз — как артефакт первой отрисовки.

Отказ: куда делся onClick

Инцидент с кнопкой Copy обобщается. onClick={copy} на кнопке внутри серверного компонента означает, что в пейлоад должна попасть функция — а функции не сериализуются (что должен получить браузер — исходник? её замыкание? её хэндл базы данных?). Фреймворки падают быстро и в dev: «Event handlers cannot be passed to Client Component props». Лечение — никогда не вешать 'use client' на всю страницу: это утащит каждый импорт страницы обратно в бандл и отменит миграцию. Выделите минимальный интерактивный лист (CopyButton.tsx, десять строк, 'use client' сверху) и оставьте статью на сервере. Запах на ревью: 'use client' в шапке файла страницы на 400 строк, добавленный ради одной кнопки.

Викторина

Коллега говорит: у нас уже есть SSR, значит, у нас уже есть всё, что даёт RSC, — компоненты и так рендерятся на сервере. Что здесь не так?

Вспомните перед уходом
  1. 01
    Проследите, что происходит при первой загрузке и при последующей навигации в RSC-фреймворке — назовите каждый артефакт, пересекающий сеть, и что браузер с ним делает.
  2. 02
    Почему именно серверный компонент может быть async-функцией с await базы данных, но не может держать useState или onClick — и каково правильное лечение, когда ему нужна кнопка?
Итог

Модель RSC растягивает одно React-дерево между двумя средами с противоположными жизненными циклами. Серверные компоненты — поведение по умолчанию — выполняются раз на запрос на сервере: они могут быть async и ждать базу или файловую систему напрямую, их импорты не стоят бандлу ничего (здесь и живут главные выигрыши: подсветка синтаксиса, markdown-конвейеры, данные локалей — сотни кБ, которые пользователь не скачивает), и они не могут держать состояние, эффекты и обработчики, потому что к моменту появления пользователя их уже нет. Клиентские компоненты, помеченные директивой use client, собираются в бандл, гидрируются и живут в браузере с полным интерактивным арсеналом. По проводу едет Flight-пейлоад: сериализованное дерево элементов, где серверные компоненты присутствуют только своим разрешённым выводом, а клиентские — дырками: ссылка на модуль, связывающая чанк и экспорт, плюс сериализованные пропсы. Это не HTML, и он стримится. SSR — отдельный компонуемый слой: при первой загрузке пейлоад питает SSR-проход, который рендерит всё дерево в HTML для первой отрисовки, после чего гидрация будит только клиентские компоненты; при каждой последующей навигации клиент забирает один Flight, и React реконсилирует его в живой DOM, сохраняя клиентское состояние на месте. Канонический отказ — интерактивность в серверном компоненте: onClick — это функция, которая не может сериализоваться в пейлоад, и dev-ошибка говорит ровно это. Лечение — минимальный лист с use client, а не директива на уровне страницы, утаскивающая весь граф импортов обратно в бандл. Теперь, когда встретишь use client в шапке 400-строчного файла страницы, ты будешь знать точную цену этого решения — и как его исправить.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.