React 19 Suspense та Concurrent Rendering: Streaming SSR і питання для співбесід 2026

Повний посібник з React 19 Suspense, concurrent rendering та streaming SSR. Вивчіть use() API, useTransition, useDeferredValue та підготуйтеся до технічних співбесід.

React 19 Suspense та Concurrent Rendering діаграма

React 19 Suspense докорінно змінює спосіб, яким застосунки обробляють асинхронні операції. Він надає декларативну модель для керування станами завантаження, обробки помилок та отримання даних. У поєднанні з concurrent rendering, Suspense забезпечує streaming SSR, який прискорює відображення контенту, зберігаючи чутливість інтерфейсу під час інтенсивних обчислень.

Ключова інформація

Межі Suspense визначають, де з'являються fallback-и завантаження. Concurrent rendering дозволяє React переривати низькопріоритетні завдання, щоб зберегти чутливість високопріоритетних взаємодій (введення, кліки). Разом вони забезпечують прогресивну гідрацію та потокову передачу HTML із сервера.

Відмінності між Concurrent Rendering та синхронним React

До React 18 рендеринг був синхронним: коли React починав рендерити дерево компонентів, він мусив завершити його перед обробкою будь-якої іншої роботи. Concurrent rendering ламає цю модель, дозволяючи React призупиняти, переривати та відновлювати рендеринг на основі пріоритетів.

Документація React описує concurrent rendering як фоновий механізм, що забезпечує такі функції, як Suspense, transitions та streaming SSR. Ключова ідея: React може готувати кілька версій UI одночасно.

TransitionExample.tsxtsx
import { useState, useTransition } from 'react';

function SearchResults({ query }: { query: string }) {
  // Витратні обчислення або отримання даних
  const results = searchDatabase(query);
  return <ResultsList items={results} />;
}

export function SearchPage() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    // Високий пріоритет: негайне оновлення input
    setQuery(value);
    // Низький пріоритет: оновлення результатів у фоні
    startTransition(() => {
      setDeferredQuery(value);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <SearchResults query={deferredQuery} />
    </div>
  );
}

Хук useTransition позначає оновлення стану як неневідкладні. React продовжує показувати попередній UI, обчислюючи новий у фоні, а потім замінює його, коли готовий.

Межі Suspense та ієрархія завантаження

Межі Suspense визначають fallback UI для компонентів, що призупиняються. Коли компонент всередині межі Suspense кидає Promise (сигналізуючи, що дані завантажуються), React показує fallback до розв'язання Promise.

ProductPage.tsxtsx
import { Suspense } from 'react';

function ProductDetails({ id }: { id: string }) {
  // Цей компонент призупиняється під час отримання
  const product = use(fetchProduct(id));
  return <div>{product.name}</div>;
}

function ProductReviews({ id }: { id: string }) {
  const reviews = use(fetchReviews(id));
  return <ReviewsList reviews={reviews} />;
}

export function ProductPage({ id }: { id: string }) {
  return (
    <div>
      {/* Зовнішня межа: ловить обидва за потреби */}
      <Suspense fallback={<PageSkeleton />}>
        {/* Внутрішня межа: відгуки можуть завантажуватись незалежно */}
        <ProductDetails id={id} />
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews id={id} />
        </Suspense>
      </Suspense>
    </div>
  );
}

Вкладені межі Suspense забезпечують гранулярний контроль станів завантаження. Деталі продукту можуть рендеритись одразу після завантаження, тоді як відгуки показують власний скелетон.

API use() для отримання даних

React 19 представляє API use(), задокументоване в React RFC, як офіційний спосіб читання Promise та контексту під час рендерингу. На відміну від хуків, use() можна викликати умовно.

UserProfile.tsxtsx
import { use, Suspense } from 'react';

// Promise створений поза компонентом (render-as-you-fetch)
const userPromise = fetchUser(userId);

function UserProfile() {
  // use() призупиняється до розв'язання Promise
  const user = use(userPromise);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export function UserPage() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile />
    </Suspense>
  );
}

Патерн render-as-you-fetch починає отримання даних до початку рендерингу, зазвичай у завантажувачах маршрутів або серверних компонентах. Це зберігає паралелізм, оскільки кілька запитів можуть виконуватись одночасно, а не каскадно.

Готовий до співбесід з React / Next.js?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Streaming SSR з React Server Components

Streaming SSR надсилає HTML клієнту частинами, коли дані стають доступними. У поєднанні з React Server Components, цей підхід забезпечує швидше перше відображення контенту з прогресивною гідрацією інтерактивних частин.

app/products/[id]/page.tsx (Next.js App Router)tsx
import { Suspense } from 'react';

// Server Component: працює тільки на сервері
async function ProductInfo({ id }: { id: string }) {
  const product = await db.products.findUnique({ where: { id } });
  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </section>
  );
}

// Server Component з повільними даними
async function RelatedProducts({ categoryId }: { categoryId: string }) {
  const products = await db.products.findMany({
    where: { categoryId },
    take: 6
  });
  return <ProductGrid products={products} />;
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await db.products.findUnique({ 
    where: { id: params.id } 
  });

  return (
    <main>
      {/* Потік починається негайно */}
      <ProductInfo id={params.id} />
      
      {/* Потік коли готово, спочатку показує скелетон */}
      <Suspense fallback={<GridSkeleton />}>
        <RelatedProducts categoryId={product.categoryId} />
      </Suspense>
    </main>
  );
}

Сервер спочатку передає HTML ProductInfo. Коли дані RelatedProducts готові, React надсилає додатковий HTML з інструкціями для заміни скелетона.

useDeferredValue для витратних обчислень

Поки useTransition обгортає оновлення стану, useDeferredValue відкладає рендеринг конкретного значення. Це добре працює для фільтрації великих списків або витратних похідних обчислень.

FilterableList.tsxtsx
import { useState, useDeferredValue, useMemo } from 'react';

function ExpensiveList({ filter }: { filter: string }) {
  // Витратна операція фільтрації
  const filteredItems = useMemo(() => {
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export function SearchableList() {
  const [filter, setFilter] = useState('');
  // Відкладена версія відстає під час введення
  const deferredFilter = useDeferredValue(filter);
  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Пошук..."
      />
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        <ExpensiveList filter={deferredFilter} />
      </div>
    </div>
  );
}

Input залишається чутливим, оскільки React пріоритизує оновлення стану. Список рендериться з відкладеним значенням, коли React має вільний час.

Питання для співбесід: Concurrent Rendering

Технічні співбесіди на позиції React часто перевіряють розуміння concurrent функцій. Ці питання відрізняють кандидатів, які впроваджували продакшн код з Suspense, від тих, хто знає лише теорію.

П: Що відбувається, коли компонент призупиняється?

Коли компонент кидає Promise під час рендерингу, React перехоплює його на найближчій межі Suspense. React рендерить fallback UI та підписується на Promise. Коли він розв'язується, React повторно рендерить призупинений компонент. Якщо Promise відхиляється, помилка поширюється до найближчої межі помилок.

П: Чому render-as-you-fetch важливий для Suspense?

Отримання під час рендерингу створює каскади запитів: Parent отримує, потім рендерить Child, який отримує, потім рендерить Grandchild, який отримує. Кожен крок чекає попередній. Render-as-you-fetch починає всі запити до рендерингу, тому вони виконуються паралельно. Завантажувачі маршрутів і серверні компоненти природно підтримують цей патерн.

П: Коли useTransition може зашкодити продуктивності?

Transitions зберігають старий UI видимим під час обчислення нового. Це означає, що React рендерить двічі: один раз для застарілого UI і один раз для свіжого. Для швидких операцій ці накладні витрати перевищують користь. Використовуйте transitions лише для оновлень, що займають помітний час, зазвичай 100мс або більше.

Error Boundaries з Suspense

Suspense обробляє стани завантаження, але error boundaries обробляють помилки. Продакшн застосунки потребують обох:

DataBoundary.tsxtsx
import { Component, Suspense, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback: ReactNode;
  errorFallback: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return this.props.errorFallback;
    }
    return this.props.children;
  }
}

export function DataBoundary({ 
  children, 
  fallback, 
  errorFallback 
}: Props) {
  return (
    <ErrorBoundary errorFallback={errorFallback}>
      <Suspense fallback={fallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

Error boundary обгортає межу Suspense, тому відхилені Promise запускають UI помилки замість краху застосунку.

Suspense Batching у React 19.2

React 19.2 представив batching Suspense для серверного рендерингу, вирішуючи поширену проблему: кілька сусідніх компонентів, що призупиняються, спричиняли кілька round trip-ів. З batching React коротко чекає, щоб зібрати кілька призупинень, а потім розв'язує їх разом.

До 19.2: Кожна межа Suspense передає потік незалежноtsx
// Після 19.2: React групує близькі призупинення

export function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {/* Ці тепер групуються разом */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <UsersCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <OrdersCard />
      </Suspense>
    </div>
  );
}

Batching зменшує візуальне мерехтіння карток, що з'являються одна за одною, та покращує сприйняту продуктивність, представляючи пов'язані дані разом.

Патерни продуктивності для продакшну

Ці патерни вирішують поширені продакшн проблеми з Suspense та concurrent rendering.

Уникайте каскадів Suspense

tsx
// Проблема: Послідовне отримання
function Parent() {
  const data = use(fetchParent());
  return <Child parentId={data.id} />; // Child отримує після Parent
}

// Рішення: Паралельне отримання з Promise.all
const [parentPromise, childPromise] = [
  fetchParent(),
  fetchChild()
];

function OptimizedParent() {
  const parent = use(parentPromise);
  return <OptimizedChild promise={childPromise} />;
}

Preload при hover

LinkWithPrefetch.tsxtsx
function LinkWithPrefetch({ href, children }: Props) {
  const router = useRouter();
  
  function handleMouseEnter() {
    // Почати отримання до кліку
    router.prefetch(href);
  }

  return (
    <Link href={href} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  );
}

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Ключові патерни для concurrent функцій React 19

  • Розміщуйте межі Suspense на швах UI, де незалежне завантаження має сенс, а не навколо кожного async компонента
  • Запускайте отримання даних у завантажувачах маршрутів або серверних компонентах, а не всередині функцій render, щоб забезпечити паралельні запити
  • Використовуйте useTransition для навігацій та відправок форм, ініційованих користувачем, що тривають понад 100мс
  • Застосовуйте useDeferredValue для витратних похідних обчислень, як-от фільтрація понад 10 000 елементів
  • Поєднуйте error boundaries з межами Suspense, щоб відхилені Promise показували UI помилки замість краху
  • Віддавайте перевагу streaming SSR для сторінок з великою кількістю контенту, де час до першого байта важливіший за повну гідрацію
  • Тестуйте concurrent функції з React DevTools Profiler для виявлення непотрібних повторних рендерів під час transitions
Щоденний виклик

Чи знайдеш ти помилку в React / Next.js?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 21 серпня 2026 р.

Теги

#react
#suspense
#concurrent-rendering
#streaming-ssr
#interview

Поділитися

Пов'язані статті