React Concurrent Rendering: как работает, когда помогает и где ломают производительность

Подробный разбор Concurrent Rendering в React: scheduler, transition updates, Suspense, useDeferredValue, production-паттерны и типичные ошибки на интервью.

04 марта 2026 г.18 минLexicon Team

Concurrent Rendering в React часто объясняют слишком абстрактно: "React теперь рендерит умнее". На практике это не магическая кнопка ускорения, а модель планирования работы, где обновления UI получают разный приоритет.

Если коротко: пользовательский ввод должен оставаться мгновенным, даже когда в фоне происходит тяжелый рендер. Для этого React может прерывать рендер, откладывать часть работы и возвращаться к ней позже.

Понимание этой механики важно не только для production-кода, но и для технического интервью. Вопросы про startTransition, useDeferredValue и Suspense сегодня идут связкой с темами производительности, ререндеров и архитектуры.

Граница между инфраструктурным Context и полноценным state management разобрана в статье React Context API: когда использовать, а когда выбрать другое решение.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Зачем вообще понадобился Concurrent Rendering

Классический сценарий проблемы:

  • пользователь печатает в поле поиска;
  • на каждый символ вы фильтруете 10 000 строк;
  • одновременно обновляется несколько дочерних компонентов;
  • ввод начинает "подлагивать".

При полностью синхронном подходе React вынужден завершить текущий рендер перед тем, как обработать следующий важный event. В результате срочное действие пользователя конкурирует с тяжелым обновлением дерева.

Concurrent Rendering решает именно эту конкуренцию:

  • срочные обновления (urgent) обрабатываются первыми;
  • менее срочные (transition) могут подождать;
  • длинная рендер-работа может быть прервана и продолжена.

Это напрямую связано с тем, когда компонент действительно перерисовывается: конкурентная модель не отменяет ререндеры, но меняет порядок и приоритет их выполнения.

Как это работает внутри React: scheduler и приоритеты

Важно разделять две вещи:

  • Render phase: вычисление нового дерева.
  • Commit phase: применение изменений в DOM.

Concurrent-подход в основном влияет на render phase. React может:

  1. Начать рендер низкоприоритетного обновления.
  2. Получить более срочный event (например, ввод текста).
  3. Прервать текущую работу.
  4. Быстро обработать срочную задачу.
  5. Вернуться к отложенному рендеру.

Если говорить инженерно, React управляет очередями обновлений с разными приоритетами (lanes). Это не многопоточность в браузере и не parallel rendering в CPU-смысле. Работа по-прежнему идет в одном main thread, но планируется иначе.

Поэтому корректнее говорить: Concurrent Rendering повышает отзывчивость интерфейса, а не "ускоряет всё приложение" автоматически.

startTransition и useTransition: отделяем срочное от несрочного

Самый практичный API для этой модели - startTransition.

import { useMemo, useState, startTransition } from "react";

type User = { id: string; name: string };

export function UserSearch({ users }: { users: User[] }) {
  const [input, setInput] = useState("");
  const [query, setQuery] = useState("");

  const filtered = useMemo(() => {
    const q = query.toLowerCase();
    return users.filter((u) => u.name.toLowerCase().includes(q));
  }, [users, query]);

  function onChange(value: string) {
    // Срочное обновление: input должен реагировать мгновенно.
    setInput(value);

    // Несрочное обновление: перерасчет большого списка можно отложить.
    startTransition(() => {
      setQuery(value);
    });
  }

  return (
    <section>
      <input value={input} onChange={(e) => onChange(e.target.value)} placeholder="Поиск пользователя" />
      <p>Найдено: {filtered.length}</p>
      <ul>
        {filtered.map((u) => (
          <li key={u.id}>{u.name}</li>
        ))}
      </ul>
    </section>
  );
}

Здесь критично, что input и тяжелая фильтрация живут в разных очередях приоритета.

Если нужен статус "идет отложенное обновление", используйте useTransition:

import { useState, useTransition } from "react";

export function ProductFilter() {
  const [tab, setTab] = useState("all");
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab: string) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <nav>
        <button onClick={() => selectTab("all")}>Все</button>
        <button onClick={() => selectTab("popular")}>Популярные</button>
      </nav>
      {isPending && <span>Обновляем список...</span>}
      <HeavyProductsView tab={tab} />
    </>
  );
}

isPending особенно полезен для UX, когда пользователь должен видеть, что система приняла действие, даже если рендер результата еще не завершен.

useDeferredValue: отложенное значение вместо отложенного setState

useDeferredValue полезен, когда есть дорогое вычисление от значения, которое меняется слишком часто.

import { useDeferredValue, useMemo, useState } from "react";

export function SearchWithDeferredList({ rows }: { rows: string[] }) {
  const [text, setText] = useState("");
  const deferredText = useDeferredValue(text);

  const visibleRows = useMemo(() => {
    const q = deferredText.trim().toLowerCase();
    if (!q) return rows;
    return rows.filter((r) => r.toLowerCase().includes(q));
  }, [rows, deferredText]);

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Фильтр" />
      <LargeVirtualizedList rows={visibleRows} />
    </>
  );
}

Отличие от startTransition:

  • startTransition вы используете в месте записи состояния;
  • useDeferredValue вы используете в месте чтения значения.

Оба подхода можно комбинировать, но без необходимости не усложняйте поток данных.

Suspense и Concurrent Rendering

Suspense и concurrent-модель тесно связаны в пользовательском опыте: React может показывать fallback и не блокировать критичные части интерфейса, пока тяжелый фрагмент еще готовится.

Пример с ленивой загрузкой:

import { Suspense, lazy } from "react";

const AnalyticsPanel = lazy(() => import("./AnalyticsPanel"));

export function DashboardPage() {
  return (
    <main>
      <Header />
      <SummaryCards />

      <Suspense fallback={<PanelSkeleton />}>
        <AnalyticsPanel />
      </Suspense>
    </main>
  );
}

Практический смысл:

  • критичный контент (Header, SummaryCards) доступен сразу;
  • тяжелый модуль загружается отдельно;
  • пользователь не видит "замороженный" экран.

С точки зрения архитектуры это продолжение темы жизненного цикла компонента в React: важно понимать, какие части дерева монтируются сразу, какие откладываются и как это влияет на UX.

Архитектурная схема для production

На крупных экранах обычно работают три уровня состояния:

  1. Urgent UI state: input, active tab, hover, фокус.
  2. Transition state: тяжелые вычисления, вторичные панели, списки.
  3. Server/data state: API-данные и кэш.

Пример потока:

  • пользователь печатает в search input;
  • input обновляется срочно;
  • фильтрация каталога запускается как transition;
  • тяжелая аналитическая панель находится за Suspense boundary;
  • commit критичных элементов не блокируется вторичной отрисовкой.

Такой дизайн хорошо стыкуется с выбором state manager. Для глобальных инфраструктурных данных достаточно Context, а критерии выбора для насыщенного доменного состояния собраны в Redux vs Zustand vs Context в React: что выбрать в 2026.

Сравнение: синхронный рендер и concurrent-подход

КритерийСинхронный подходConcurrent Rendering
Реакция input под нагрузкойМожет деградироватьОбычно стабильнее
Приоритизация обновленийОграниченнаяЯвная через transition
Контроль UX при тяжелых участкахСложнееЛучше с Suspense/useTransition
Сложность отладкиНижеВыше из-за приоритетов
Риск неверного использованияСреднийВыше при хаотичном применении
Эффект без архитектурной дисциплиныНизкийНизкий

Ключевая мысль: concurrent-инструменты сильны только вместе с нормальной декомпозицией компонентов и контролем ререндеров. Если у вас проблемный baseline, сначала исправьте его.

Production pitfalls: где чаще всего ломают

1. Маркируют transition всё подряд

Ошибка: почти каждый setState оборачивают в startTransition, надеясь "ускорить" экран.

Что происходит:

  • приоритеты размываются;
  • поведение интерфейса становится сложнее предсказать;
  • диагноз проблем усложняется.

Исправление: transition нужен только там, где есть реальная конкуренция между срочной и несрочной работой.

2. Ожидают, что transition решит проблемы commit-phase

Если bottleneck в тяжелом DOM-commit, layout thrashing или дорогих эффектах после commit, transition не спасет автоматически.

Исправление:

  • уменьшайте объем commit;
  • разбивайте дерево на более мелкие границы;
  • выносите тяжелые компоненты за Suspense/lazy;
  • проверяйте профилировщик до и после изменений.

3. Используют useDeferredValue без профита

Если вычисления дешевые, deferred-слой только усложняет логику и создает ложное ощущение оптимизации.

Исправление: измеряйте p95 времени ввода и длительность render-коммитов перед внедрением.

4. Забывают о стабильности ссылок и мемоизации

Concurrent Rendering не отменяет базовые правила React. Если вы каждый рендер создаете новые объекты, callbacks и массивы в горячих местах, ререндеры останутся дорогими.

Стабилизацию ссылок и мемоизацию в таких сценариях удобно сверять с разбором React.memo, useMemo и useCallback: оптимизация без магии.

Как измерять эффект, а не верить ощущениям

Минимальный набор наблюдаемости:

  1. React DevTools Profiler:
  • сколько времени занимает render у узлов;
  • какие компоненты ререндерятся чаще ожидаемого;
  • как меняется картина после startTransition.
  1. Пользовательские метрики:
  • задержка между keydown и визуальным обновлением input;
  • время появления результатов поиска;
  • длительность блокирующих задач в main thread.
  1. Сценарные тесты:
  • медленное устройство;
  • список 5k-20k элементов;
  • одновременный ввод + network activity.

Если измерений нет, решение остается предположением.

Best practices для Concurrent Rendering

  1. Разделяйте urgent и transition обновления только в горячих user-flow.
  2. Не используйте transition как универсальную обертку для state.
  3. Ставьте Suspense boundary вокруг дорогих участков UI.
  4. Проверяйте, что fallback не ломает пользовательский сценарий.
  5. Держите провайдеры и крупные контейнеры максимально локальными.
  6. Стабилизируйте props для тяжелых дочерних компонентов.
  7. Проверяйте производительность на медленных девайсах, не только на ноутбуке разработчика.
  8. Закладывайте архитектуру рендера на уровне экрана, а не только компонента.

Частые ошибки на собеседовании

  • Путать concurrent rendering с многопоточностью JavaScript.
  • Говорить, что React "рендерит параллельно на нескольких ядрах".
  • Не уметь объяснить разницу между startTransition и useDeferredValue.
  • Не связывать concurrent-инструменты с реальными метриками UX.
  • Думать, что переход на React 18/19 автоматически решает лаги.

Сильный сигнал для интервьюера: вы объясняете не только API, но и условия, при которых этот API приносит пользу.

Как отвечать на интервью уверенно

Рабочая структура ответа:

  1. Concurrent Rendering - это приоритизация и прерываемость рендера, а не параллельное выполнение на разных потоках.
  2. Срочные обновления (ввод, взаимодействие) должны оставаться отзывчивыми.
  3. Несрочные обновления переносим в transition (startTransition, useTransition) или используем useDeferredValue.
  4. Для тяжелых частей UI применяем Suspense boundary.
  5. Эффект подтверждаем профилированием, а не визуальными ощущениями.

Такой ответ показывает, что вы понимаете системную механику React, а не только синтаксис хуков.

FAQ

Concurrent Rendering включается отдельно?

В современных React-приложениях с createRoot concurrent-возможности доступны по умолчанию, но поведение зависит от того, используете ли вы API вроде startTransition, useDeferredValue и Suspense.

Нужно ли добавлять startTransition во все фильтры и сортировки?

Нет. Сначала измерьте, действительно ли есть деградация отзывчивости. Если пользователь не чувствует задержки, дополнительная сложность может быть не нужна.

Может ли Concurrent Rendering ухудшить UX?

Да, если неправильно расставлены приоритеты и fallback-поведение. Например, при агрессивном использовании transitions можно получить слишком долгие вторичные обновления без понятной обратной связи.

Concurrent Rendering заменяет оптимизацию компонентов?

Нет. Он дополняет базовые практики: декомпозицию, memoization, правильные key, ограничение области обновлений и адекватную работу со state.

Следующий шаг

Чтобы закрепить тему, полезно пройти практические кейсы: поиск по большому списку, тяжелый dashboard и форма с конкурентными обновлениями. На таких задачах видно, где concurrent-подход действительно дает выигрыш.

Практика реальных технических собеседований по React

Тренажер с живыми React-вопросами: concurrent rendering, производительность интерфейсов и примеры качественных ответов.

Перейти к практике собеседований

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Автор

Lexicon Team

Читайте также