javascript

RSC Explorer: что на самом деле летит по сети в React Server Components

  • пятница, 26 декабря 2025 г. в 00:00:04
https://habr.com/ru/articles/980494/

Команда JavaScript for Devs подготовила перевод статьи о том, как на самом деле работают React Server Components. Автор разбирает RSC на уровне протокола: что именно стримится с сервера, как JSX путешествует по сети, почему состояние не ломается при обновлениях и зачем React вообще понадобился такой странный формат.


За последние несколько недель, после раскрытия критической уязвимости безопасности в React Server Components (RSC), интерес к протоколу RSC заметно вырос.

Протокол RSC — это формат, в котором React сериализует и десериализует деревья React (а также расширение JSON). React предоставляет и писатель, и читатель для протокола RSC; они версионируются и развиваются синхронно друг с другом.

Поскольку протокол RSC — это внутренняя реализация React, он прямо не документирован за пределами исходного кода. Плюс такого подхода в том, что у React есть большая свобода улучшать формат, добавлять новые фичи и оптимизации.

Однако есть и минус: даже люди, которые активно создают приложения на React Server Components, зачастую плохо представляют, как всё это работает под капотом.

Несколько месяцев назад я написал Progressive JSON, чтобы объяснить некоторые идеи, лежащие в основе протокола RSC. Хотя вам и не «обязательно» их знать, чтобы использовать RSC, это как раз тот случай, когда заглянуть внутрь действительно интересно и полезно.

Хотелось бы, чтобы рост интереса сейчас был вызван другими обстоятельствами, но так или иначе, именно он вдохновил меня сделать небольшой инструмент, который наглядно показывает, как всё работает.

Я назвал его RSC Explorer — он доступен по адресу https://rscexplorer.dev/.

Разумеется, это open source.


Как говорится, «лучше один раз показать, чем сто раз рассказать». Вот он, прямо здесь, во встроенном виде.

Начнём с Hello World:

Обратите внимание на строку, подсвеченную жёлтым, с чем-то на первый взгляд непонятным. Если присмотреться, это Hello, представленное в виде фрагмента JSON. Эта строка — часть RSC-потока с сервера. Именно так React «разговаривает сам с собой» по сети.

Теперь нажмите большую жёлтую кнопку «step».

Обратите внимание, что Hello появился справа. Это JSX, который клиент восстановил после чтения этой строки. Мы только что увидели, как простой кусок JSX — тег Hello — прошёл по сети и был «оживлён» на другой стороне.

Ну, строго говоря, не совсем «прошёл по сети».

Одна из классных особенностей RSC Explorer в том, что это одностраничное приложение: оно полностью работает в браузере (точнее, серверная часть крутится в worker’е). Поэтому, если вы откроете вкладку Network, запросов вы не увидите. В каком-то смысле это симуляция.

Тем не менее RSC Explorer использует ровно те же пакеты, которые React предоставляет для чтения и записи протокола RSC, так что каждая строка вывода — настоящая.

Async Component

Попробуем что-нибудь чуть более интересное, чтобы увидеть стриминг в действии.

Возьмите этот пример и нажмите большую жёлтую кнопку «step» ровно два раза:

(Если сбились со счёта, нажмите «restart» слева, а затем снова «step» два раза.)

Посмотрите на верхнюю правую панель. Вы увидите три чанка в формате протокола RSC (который, повторюсь, вам вовсе не обязательно уметь читать — да и между версиями он меняется). Справа показано то, что Client React успел восстановить на данный момент.

Обратите внимание на «дыру» в середине стримящегося дерева, визуализированную в виде плашки «Pending».

По умолчанию React не показывает неконсистентный UI с такими «дырами». Однако поскольку вы объявили состояние загрузки с помощью , теперь может отображаться частично готовый интерфейс (обратите внимание: уже виден, но показывает fallback-контент, потому что ещё не успел прийти по стриму).

Нажмите кнопку «step» ещё раз — и «дыра» заполнится.

Counter

До сих пор мы отправляли на клиент только данные; теперь давайте отправим ещё и код.

В качестве классического примера используем счётчик.

Нажмите большую жёлтую кнопку «step» два раза:

Это обычный старый добрый счётчик — вроде бы ничего особенно интересного.

Или всё-таки есть?

Посмотрите на полезную нагрузку протокола. Читать её непросто, но обратите внимание: мы не отправляем строку "Count: 0", не отправляем и не отправляем HTML. Мы отправляем сам — то есть «виртуальный DOM». Его, конечно, можно потом превратить в HTML, как и любой JSX, но это вовсе не обязательно.

Как будто мы возвращаем деревья React из API-роутов.

Обратите внимание, как ссылка на Counter превращается в ["client",[],"Counter"] в протоколе RSC — это означает «возьми экспорт Counter из клиентского модуля». В реальном фреймворке этим занимается бандлер, поэтому RSC и интегрируется с бандлерами. Если вы знакомы с webpack, это похоже на чтение из require-кэша webpack. (Собственно, именно так это реализовано в RSC Explorer.)

Form Action

Мы только что увидели, как сервер ссылается на кусок кода, предоставленный клиентом.

Теперь посмотрим на обратную ситуацию: клиент ссылается на код, предоставленный сервером.

Здесь greet — это Server Action, опубликованный как эндпоинт с помощью 'use server'. Он передаётся как проп в клиентский компонент Form, который видит его как асинхронную функцию.

Нажмите большую жёлтую кнопку «step» три раза:

Теперь введите своё имя в панели Preview и нажмите «Greet». Отладчик RSC Explorer снова «поставит на паузу», показывая, что мы попали в Server Action greet с запросом. Нажмите жёлтую кнопку «step», чтобы увидеть ответ, возвращённый клиенту.

Router Refresh

RSC часто объясняют в контексте фреймворка, но это скрывает происходящее внутри. Например, как фреймворк обновляет серверный контент? Как вообще работает роутер?

RSC Explorer показывает RSC без фреймворка. Здесь нет router.refresh — но вы можете реализовать собственный refresh Server Action и компонент Router.

Нажимайте кнопку «step», пока на экране не появится весь начальный UI:

Посмотрите на тикающий таймер. Обратите внимание, как компонент ColorTimer с сервера передал случайный цвет компоненту Timer на клиенте. Снова сервер вернул (или что-то в этом духе).

Теперь нажмите кнопку Refresh прямо над таймером.

Не вникая в код, «пройдитесь» кнопкой «step» по серверному ответу и посмотрите, что происходит. Вы увидите, как непрерывно тикающий Timer получает новые пропсы от сервера. Его цвет фона меняется, но состояние сохраняется.

В каком-то смысле это похоже на повторную загрузку HTML с помощью чего-нибудь вроде htmx, только здесь это обычное обновление React «виртуального DOM», которое не уничтожает состояние. Компонент просто получает новые пропсы… с сервера. Нажмите «Refresh» несколько раз и пошагово посмотрите, что происходит.

Если хотите разобраться, как это работает под капотом, прокрутите вниз серверную и клиентскую части. Вкратце: клиентский Router хранит Promise на серверный JSX, который возвращает renderPage(). Изначально renderPage() вызывается на сервере (для первого рендера), а затем — на клиенте (для повторных запросов).

Эта техника, в сочетании с сопоставлением URL и вложенностью, по сути и есть то, как RSC-фреймворки реализуют роутинг. На мой взгляд, это очень крутой пример.

Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!