open atlas
↑ К треку
Архитектура фронтенда FE · 02 · 06

Senior internals: RSC payload, слои кэша и production падения

Формат RSC Payload по wire, Server Functions как типобезопасные API endpoints, инвалидация многослойного кэша, hydration несоответствия и реальные production падения от Vercel, Spotify и Linear.

FE Senior ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

После RSC миграции у Vercel, TTFB клиента регрессировал со 100ms до 2.5s. Дерево компонентов выглядело корректно. Data fetching выглядел корректно. Виновник: Server Component который fetch’ил 50 элементов в последовательном цикле вместо Promise.all. Паттерн невидимый на code review, но катастрофический в production.

Формат RSC Payload по wire

После этого урока ты сможешь объяснить формат RSC Payload, восстановить цепочку многослойного кэш-бага и обнаружить hydration-несоответствие до того, как его зарепортит пользователь.

Когда React рендерит дерево Server Component’ов, вывод — не HTML и не JSON. Это RSC Payload — кастомный стримящийся формат разработанный для инкрементальной обработки по мере прихода по сети.

Формат использует row-prefix коды:

  • M — module reference (URL bundle клиентского компонента)
  • J — JSON literal (сериализованные props или данные)
  • H — hint (директива браузера типа prefetch)

Каждая строка ссылается на предыдущие строки по ID, инкрементально строя React-дерево. Пример:

0:["$","div",null,{"children":[["$","h1",null,{"children":"Товар"}]]}]
1:{"id":42,"name":"Товар","price":29.99}

React клиент браузера обрабатывает эти строки по мере их прихода, включая то же “стримить HTML, рендерить что есть” поведение описанное в уроке про Suspense — но для обновлений React-дерева, не только начального HTML.

RSC Payload — механизм за Next.js client-side навигацией: вместо загрузки новой HTML страницы, клиент fetch’ит RSC Payload для нового роута и применяет к существующему React-дереву. Это делает навигации мгновенными потому что шелл (лэйаут, nav) никогда не перерисовывается.

Server Functions: типобезопасный RPC через HTTP

React 19 вводит Server Functions — async функции помеченные 'use server' которые могут вызываться из Client Components, но выполняются на сервере. Фреймворк генерирует стабильный POST endpoint для каждой; client-side вызов становится типизированным fetch.

// actions.ts
'use server';
export async function likePost(postId: string) {
  await db.post.update({ where: { id: postId }, data: { likes: { increment: 1 } } });
  revalidatePath('/posts');
}

// PostCard.tsx — Client Component
'use client';
import { likePost } from './actions';
export function PostCard({ postId }) {
  return <button onClick={() => likePost(postId)}>Лайк</button>;
}

TypeScript видит likePost как обычную типизированную функцию. Не нужно определять REST роут, типы request/response или форму обработки ошибок. Аргументы сериализуются автоматически; тело функции никогда не попадает в клиент.

В сочетании с form actions (<form action={serverFunc}>), Server Functions включают progressive enhancement: форма работает без JavaScript потому что браузер сабмитит на сгенерированный endpoint напрямую.

Ограничения: аргументы должны быть сериализуемы (нет class instances, нет функций). Аутентификация должна быть явной внутри функции. Никогда не доверять client-предоставленным ID для проверок владения без повторной валидации на сервере.

Слои кэша и координированная инвалидация

Современное Next.js 15 приложение имеет минимум пять слоёв кэширования:

  1. CDN edge кэш — по URL, секунды до часов. Обслуживает статический контент без обращения к origin.
  2. Next.js fetch кэш — вызовы RSC fetch() кэшируются по умолчанию с настраиваемым revalidate периодом.
  3. Database query кэш — Redis или in-memory, перед базой данных.
  4. Браузерный HTTP кэш — контролируется Cache-Control заголовками.
  5. Client library кэш — TanStack Query / SWR, по queryKey.

Пять слоёв — пять точек отказа. Мутация, которая инвалидирует слои 1–4 но пропускает слой 5, оставляет пользователя смотреть на устаревшие данные до gcTime (по умолчанию 5 минут) — без ошибок, без предупреждений, просто неверные цифры на экране.

Мутация изменяющая данные должна инвалидировать нужные слои:

// На сервере (Server Function или route handler)
revalidatePath('/products');      // Next.js кэш + CDN если настроен
revalidateTag('product-42');      // гранулярная tag-based CDN очистка

// На клиенте
queryClient.invalidateQueries({ queryKey: ['product', 42] });

Пропуск любого слоя оставляет пользователей смотреть на устаревшие данные. Самый распространённый баг: серверный кэш инвалидирован, но клиентский TanStack Query кэш продолжает отдавать старый ответ следующие 5 минут.

Каскад инвалидации кэша при мутации
1POST /api/products/42 мутация завершена
2Сервер: revalidateTag(‘product-42’) → Next.js кэш + CDN очистка
3Клиент: queryClient.invalidateQueries([‘product’, 42]) → refetch
4Клиент: invalidateQueries([‘products’]) → listing кэш обновляется

Hydration несоответствия: тихие баги интерактивности

React’s hydration предполагает что server-rendered DOM точно совпадает с тем что клиент бы отрендерил с теми же props. Когда они расходятся, reconciler React’а пытается исправить DOM, но его модель того, какие event handlers принадлежат каким DOM узлам, теперь несогласована — клики могут срабатывать на неправильном handler’е или тихо теряться.

Распространённые причины:

  • Date.now() или Math.random() рендерятся с обеих сторон — разные значения
  • Условный рендеринг на основе window.innerWidth — сервер не имеет window
  • Чтение localStorage на уровне модуля

Видимый вывод выглядит корректно, потому что и сервер и клиент рендерят валидный HTML. Но биндинги event handler’ов не согласованы.

Паттерны фикса:

  • useEffect для client-only логики: рендерить безопасное начальное состояние с обеих сторон; изменять на client-specific состояние после завершения hydration
  • suppressHydrationWarning: валидно для genuinely расходящихся листьев (timestamps с locale)
  • Client Components для контента который фундаментально client-specific

Когда видишь кнопку, которая иногда игнорирует клик — не выбрасывает ошибку, просто ничего не делает, — подозревай hydration-несоответствие раньше, чем начнёшь читать логику event handler’а.

React 19 улучшил сообщения об ошибках hydration для локализации несовпадающего элемента; до React 19 сообщение было generic. Production команды инструментируют hydration ошибки через error boundaries отправляющие в Sentry — стабильный ненулевой rate сигнализирует о накапливающихся несоответствиях до того как они всплывут как user-reported баги.

Edge runtime для data fetching

Edge runtime (Next.js Edge, Cloudflare Workers, Vercel Edge) запускает серверный код на CDN узлах вместо центрального дата-центра. Задержка до пользователя: единицы ms vs сотни для традиционных серверов.

Лучшее применение: глобально-реплицированные источники данных (Cloudflare D1, Turso, PlanetScale) где edge сервер fetch’ит из ближнего реплики. Edge-rendered RSC даёт первый paint за 50–100ms независимо от географии.

Ограничения: ограниченные API (нет Node.js fs, нет нативных модулей), только Web-стандартные API (fetch, crypto.subtle), меньшие лимиты памяти на запрос.

Почему это работает

Формат RSC Payload задокументирован в RFC репозитории React и намеренно не является JSON — он разработан для инкрементального парсинга по мере прихода байт. JSON требует полную строку перед парсингом. Формат RSC позволяет React начать строить дерево компонентов с первых байт, соответствуя стримящейся модели chunked HTTP transfer. Авторы фреймворков (Next.js, Remix, Waku) реализуют серверный эмиттер; React DOM реализует клиентский консьюмер.

Реальные production падения

Vercel 2024 — server-side waterfall: RSC миграция клиента регрессировала TTFB со 100ms до 2.5s. Корневая причина: Server Component fetch’ил 50 товаров в последовательном цикле for...await вместо Promise.all. Обнаружено observability dashboards зафиксировавшими спайк p95 TTFB; исправлено оборачиванием в Promise.all. Урок: последовательный await в Server Components — server-side waterfall с той же ценой что и client-side — он блокирует Suspense boundary от стриминга.

Spotify Web Player 2023 — неполный optimistic snapshot: Optimistic update для “добавить в плейлист” использовал setQueryData, но onMutate не делал snapshot предыдущего состояния. Когда сервер отклонил запрос, откат установил значение кэша в undefined вместо реального предыдущего состояния — плейлисты визуально исчезали на несколько секунд. Исправлено каноническим паттерном: всегда вызывать queryClient.getQueryData в onMutate перед optimistic update’ом.

Linear 2024 — потеря сообщений при SSE переподключении: Server-Sent Events based real-time синхронизация использовала client-generated схему message ID. При переподключении клиент пропускал сообщения отправленные во время окна отключения — пользователи видели как карточки “отпрыгивают” после drag операций. Исправлено server-assigned монотонными ID и протоколом возобновления на основе Last-Event-ID который сервер использует для повтора пропущенных сообщений.

Senior-tier числа data fetching
Cold start edge runtime
меньше 50 ms
Размер RSC Payload (типичная страница)
10–50 KB
HTTP/2 max concurrent streams (по умолчанию)
100
Next.js 15 App Router по умолчанию
RSC + App Router
Стабильный релиз React 19 RSC
Q4 2024
Викторина

Команда мигрирует с CSR на RSC. Bundle уменьшается с 240KB до 60KB, но время JS evaluation на мобильном остаётся тем же. Почему?

Викторина

Почему hydration несоответствия тихо ломают интерактивность даже когда видимый HTML выглядит корректно?

Вспомните перед уходом
  1. 01
    Что такое RSC Payload и почему он не JSON?
  2. 02
    Объясни 5 слоёв кэширования в Next.js 15 приложении и какой API обрабатывает каждый.
  3. 03
    Что такое баг optimistic update Spotify Web Player и каков фикс?
Итог

RSC Payload — стримящийся row-протокол, не HTML и не JSON, позволяющий React инкрементально строить дерево компонентов по мере прихода байт, включая навигацию без полной перезагрузки страниц. Server Functions генерируют стабильные типизированные POST endpoints из 'use server'-помеченных async функций, заменяя вручную написанные API роуты. Многослойная инвалидация кэша охватывает пять слоёв — CDN, Next.js fetch кэш, DB query кэш, HTTP кэш и client library кэш — каждый требует явной инвалидации при мутациях. Hydration несоответствия (расхождения серверного и клиентского DOM) разрушают биндинги event handler’ов тихо; исправлять откладывая client-only логику в useEffect и мониторя hydration ошибки в production. Edge runtime сокращает first-paint до 50ms но только когда источник данных глобально реплицирован — edge функции обращающиеся к single-region базе добавляют round-trip’ы, а не убирают. Теперь, когда на code review встретишь Server Function без проверки сессии внутри, — флагнешь сразу: фреймворк не добавляет авторизацию за тебя.

Практика

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

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в228

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

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

Примени это

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

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

Trademarks belong to their respective owners. Editorial reference only.