awesome-everything EN
↑ Обратно к восхождению

Архитектура фронтенда

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

Суть Формат RSC Payload по wire, Server Functions как типобезопасные API endpoints, инвалидация многослойного кэша, hydration несоответствия и реальные production падения от Vercel, Spotify и Linear.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

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

Формат RSC Payload по wire

Когда 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.

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

// На сервере (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

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 несоответствия разрушают биндинги event handler’ов тихо; исправлять откладывая client-only логику в useEffect и мониторя hydration ошибки в production. Edge runtime сокращает first-paint до 50ms но только когда источник данных глобально реплицирован — edge функции обращающиеся к single-region базе добавляют round-trip’ы, а не убирают.

Связанные уроки
встречается в202
Продолжить восхождение ↑Data fetching: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.