React Server Components и Suspense streaming
Как RSC async компоненты устраняют клиентский JS для статических данных, и как Suspense boundaries позволяют серверу стримить шелл за 100ms, пока медленные секции приходят позже.
Next.js 15 приложение с RSC может показать шелл лэйаута пользователю за 100ms, даже когда данные за ним занимают 800ms для fetch’а — без выполнения какого-либо JavaScript в браузере для этого контента. Как?
Что такое React Server Components
После этого урока ты поймёшь, почему добавление 'use client' к каждому компоненту уничтожает преимущество RSC — и как провести границу правильно.
React Server Components (RSC) — это React компоненты, которые выполняются исключительно на сервере. Они являются async функциями, которые могут обращаться к базам данных, читать файлы или вызывать любой server-side API. Их вывод — HTML плюс специальное сериализованное React-дерево (RSC Payload) — но никогда не JavaScript, отправляемый в браузер.
Ключевое разделение:
- Server Components — рендерятся на сервере, производят HTML. Без useState, без useEffect, без браузерных API. Ноль KB кода компонента в браузерном bundle.
- Client Components — помечаются
'use client'вверху файла. Запускаются и на сервере (для начального HTML) и на клиенте (для интерактивности). Их код компонента отправляется в браузер.
// ServerCard.tsx — Server Component (директива не нужна)
// Запускается только на сервере. JS в браузер не отправляется.
async function ServerCard({ id }: { id: string }) {
const data = await db.product.findUnique({ where: { id } });
return <div>{data.name}</div>;
}
// AddToCart.tsx — Client Component
'use client';
import { useState } from 'react';
export function AddToCart({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? 'Добавлено' : 'В корзину'}</button>;
}На типичной странице товара, заголовок, описание и изображение — Server Components: они fetch’ят один раз при запросе, отдают HTML, не отправляют JS. Только интерактивные листья (AddToCart, фильтры, модалки) становятся Client Components.
Как Suspense включает стриминг
React Suspense — примитив для “покажи fallback пока эта часть дерева загружается”. На сервере Suspense boundary позволяет остальному дереву стримиться, пока suspended компонент ещё разрешает своё async обещание.
Lifecycle запроса со стримингом:
- Запрос приходит на сервер
- Сервер начинает рендеринг. Шелл лэйаута (nav, заголовок страницы, skeleton-shaped fallbacks) не нуждается в async данных — рендерится мгновенно
- Чанк HTML шелла отправляется по открытому response stream. TTFB срабатывает здесь (~50–100ms)
- Сервер запускает async data fetch’и внутри Suspense boundaries, параллельно
- По мере разрешения каждого fetch’а, сервер рендерит эту секцию и стримит HTML чанк
- Браузер получает чанки, заменяет Suspense fallbacks на реальный контент
- LCP срабатывает когда самый большой визуально элемент заcтримился
Вместе эти семь шагов означают, что пользователь видит живой интерактивный шелл уже через 100ms, а не пустой экран. Без шага 3 (ранняя отправка шелла) вся цепочка блокируется на самом медленном fetch’е шага 4.
Польза: без стриминга TTFB был бы равен самому медленному fetch’у (600ms). Со стримингом TTFB равен времени рендеринга шелла (80ms). Пользователь видит контент намного раньше.
Правило границы ‘use client’
Когда подключаешь новый компонент, сопротивляйся рефлексу добавить 'use client' сверху — сначала проверь, нужно ли ему вообще какое-то состояние браузера.
Лучшая архитектура: держать как можно больше дерева как Server Components, переносить 'use client' к листьям, которым реально нужна интерактивность.
// Page.tsx — Server Component (по умолчанию)
async function ProductPage({ id }) {
const product = await fetchProduct(id); // server-side, нуль клиентского JS
return (
<main>
<h1>{product.name}</h1>
<ProductImages urls={product.images} /> {/* Server Component */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewsSection productId={id} /> {/* Server Component, стримится */}
</Suspense>
<AddToCart productId={id} /> {/* Client Component */}
</main>
);
}Влияние на размер bundle’а реально: Next.js 15 приложение, которое переносит большинство компонентов на server-side, может уменьшить клиентский bundle с 240KB до 60KB.
Ловушка dehydration/rehydration
Распространённый баг при добавлении TanStack Query в RSC приложение: сервер fetch’ит данные, рендерит HTML, отправляет. Клиент hydrates, монтирует те же query хуки и fetch’ит те же данные снова — два round-trip’а для одной загрузки страницы.
Фикс: dehydrate серверный query cache в HTML, rehydrate на клиенте:
// На сервере
const queryClient = new QueryClient();
await queryClient.prefetchQuery({ queryKey: ['product', id], queryFn: () => fetchProduct(id) });
const dehydratedState = dehydrate(queryClient);
// В HTML — передать клиенту
<HydrationBoundary state={dehydratedState}>
<ProductPage />
</HydrationBoundary>Теперь useQuery(['product', id]) на клиенте находит данные в кэше мгновенно — без повторного fetch’а.
▸Почему это работает
Утверждение RSC о “нуле JS для серверных компонентов” значимо только если ты реально его используешь. Ловушка — помечать всё 'use client' по привычке — ты получаешь тот же JS bundle что и обычный SPA, но с дополнительной сложностью. Правило: если в компоненте нет onClick, useState, useEffect и браузерных API, он должен быть Server Component.
В Next.js 15, где должен жить useState?
Без Suspense, как медленная data dependency влияет на TTFB в SSR?
Упорядочи события для RSC страницы с Suspense streaming:
- 1 Пользователь кликает по ссылке; браузер отправляет HTTP запрос на сервер
- 2 Сервер рендерит шелл лэйаута (async данные не нужны)
- 3 Чанк HTML шелла отправляется по response stream — TTFB
- 4 Сервер параллельно запускает async fetch'и внутри Suspense boundaries
- 5 Каждый fetch разрешается; сервер рендерит и стримит эту секцию
- 6 Браузер заменяет Suspense fallbacks на реальный контент по мере прихода чанков
- 7 LCP срабатывает когда самый большой визуальный элемент появляется
- 01Что такое RSC Payload и для чего он используется?
- 02Почему Suspense streaming даёт более низкий TTFB, чем блокирующий SSR?
- 03Что такое dual-fetch ловушка и как её исправить?
React Server Components выполняются целиком на сервере — без useState, без браузерных API, нуль клиентского JS. Async Server Components fetch’ят данные при запросе; Suspense boundaries позволяют шеллу лэйаута стримиться за ~80ms, пока каждая секция данных приходит независимо. Директива ‘use client’ должна быть у листьев дерева, помечая только компоненты которым реально нужна интерактивность. Mixed RSC + client cache приложения должны использовать dehydration/rehydration (дегидрацию/регидрацию серверного кэша) для избежания dual-fetch ловушки. В Next.js 15 App Router эта архитектура — по умолчанию; bundle может уменьшиться с 240KB до 60KB перенеся статические display компоненты на server-side. Теперь, когда на code review ты увидишь 'use client' на компоненте без onClick или useState, — флагнешь это сразу и предложишь конкретную замену.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в228
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Что такое воркеры и зачем они нужныjunior
- Механика web workers: dedicated, shared и OffscreenCanvasmiddle
- Structured clone и transferablesmiddle
- Жизненный цикл service worker и стратегии кешированияmiddle
- SharedArrayBuffer, Atomics и cross-origin isolationsenior
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Пулы воркеров, Comlink и наблюдаемость в продакшенеsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Что такое движок JavaScriptzero
- Ignition и байт-кодmiddle
- Внутри цикла интерпретатораmiddle
- Как V8 представляет значениеmiddle
- Что такое Map на самом деле: вскрываем hidden classmiddle
- Деревья переходов, deprecation и миграцияmiddle
- Быстрые свойства, медленные свойства и slack trackingmiddle
- Inline cache в глубину: feedback-слоты и handlersmiddle
- Monomorphic, polymorphic, megamorphic — и stub cachemiddle
- Четыре яруса и как работает разогревmiddle
- Обратная связь о типах: топливо для оптимизатораsenior
- TurboFan: оптимизатор на море узловsenior
- Спекуляция и guardssenior
- Деоптимизация: падение с обрываsenior
- On-Stack Replacement: подмена фрейма посреди циклаsenior
- Области видимости, контексты и scope chainmiddle
- Что на самом деле удерживает замыканиеmiddle
- Когда V8 аллоцирует Context (и чего это стоит)middle
- Замыкания, feedback-векторы и полиморфизм точки вызоваmiddle
- Зачем нужен GC и достижимостьmiddle
- Утечки в языке со сборкой мусораsenior
- Движок против циклаmiddle
- Микрозадачи против макрозадач: порядокmiddle
- Внутри Promisemiddle
- async/await, десахаризированныйmiddle
- Капстоун: оптимизируем горячий путьsenior
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Что делает TLS и зачем он нуженjunior
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Что такое OpenTelemetry: API, SDK, Collector, OTLPjunior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Что такое trace propagation и почему сломанная propagation хуже отсутствия трейсовjunior
- traceparent и tracestate: полный формат W3C-заголовкаmiddle
- Baggage и async-границы: перенос контекста через очереди и callback''''иmiddle
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.