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

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

React Server Components и Suspense streaming

Суть Как RSC async компоненты устраняют клиентский JS для статических данных, и как Suspense boundaries позволяют серверу стримить шелл за 100ms, пока медленные секции приходят позже.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Next.js 15 приложение с RSC может показать шелл лэйаута пользователю за 100ms, даже когда данные за ним занимают 800ms для fetch’а — без выполнения какого-либо JavaScript в браузере для этого контента. Как?

Что такое React Server Components

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 запроса со стримингом:

  1. Запрос приходит на сервер
  2. Сервер начинает рендеринг. Шелл лэйаута (nav, заголовок страницы, skeleton-shaped fallbacks) не нуждается в async данных — рендерится мгновенно
  3. Чанк HTML шелла отправляется по открытому response stream. TTFB срабатывает здесь (~50–100ms)
  4. Сервер запускает async data fetch’и внутри Suspense boundaries, параллельно
  5. По мере разрешения каждого fetch’а, сервер рендерит эту секцию и стримит HTML чанк
  6. Браузер получает чанки, заменяет Suspense fallbacks на реальный контент
  7. LCP срабатывает когда самый большой визуально элемент заcтримился
RSC streaming timeline
T=0: запрос приходит, шелл рендерится мгновенно
T=80ms: HTML шелла стримится → TTFB
T=300ms: быстрая секция готова, стримится
T=600ms: медленная секция готова → LCP

Польза: без стриминга TTFB был бы равен самому медленному fetch’у (600ms). Со стримингом TTFB равен времени рендеринга шелла (80ms). Пользователь видит контент намного раньше.

Правило границы ‘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. 1 Пользователь кликает по ссылке; браузер отправляет HTTP запрос на сервер
  2. 2 Сервер рендерит шелл лэйаута (async данные не нужны)
  3. 3 Чанк HTML шелла отправляется по response stream — TTFB
  4. 4 Сервер параллельно запускает async fetch'и внутри Suspense boundaries
  5. 5 Каждый fetch разрешается; сервер рендерит и стримит эту секцию
  6. 6 Браузер заменяет Suspense fallbacks на реальный контент по мере прихода чанков
  7. 7 LCP срабатывает когда самый большой визуальный элемент появляется
Вспомните перед уходом
  1. 01
    Что такое RSC Payload и для чего он используется?
  2. 02
    Почему Suspense streaming даёт более низкий TTFB, чем блокирующий SSR?
  3. 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.

Связанные уроки
встречается в202
Продолжить восхождение ↑Клиентский кэш: TanStack Query, SWR и stale-while-revalidate
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.