Concurrent Mode — конкурентный режим в React

16 июня 2026
Автор

Олег Марков

Введение

До появления React 18 рендеринг был полностью синхронным и блокирующим: React начинал обновление и не останавливался, пока не завершит его. Любое сложное обновление, занимающее более 16 мс, приводило к видимым «подвисаниям» интерфейса. Пользователь нажимал кнопку или вводил текст — и наблюдал за замёрзшим UI.

Concurrent Mode (конкурентный режим) — это фундаментальное изменение внутренней архитектуры React, которое позволяет рендерингу быть прерываемым. React теперь может приостановить работу над одним обновлением, обработать более приоритетное событие (например, нажатие клавиши) и вернуться к прерванному обновлению позже.

Начиная с React 18, конкурентные возможности включены по умолчанию при использовании createRoot. Это не отдельный «режим» в смысле переключателя, а набор функций, которые активируются при использовании конкурентных API: useTransition, startTransition, useDeferredValue, Suspense.

Если вы хотите глубже изучить React и его внутренние механизмы, рекомендую курс по React на PurpleSchool.

Как работает планировщик (Scheduler)

В основе Concurrent Mode лежит внутренний модуль React — Scheduler. Он отвечает за распределение работы по рендерингу во времени.

Архитектура Fiber

Concurrent Mode стал возможен благодаря переходу React с рекурсивного алгоритма «Stack Reconciler» (React 15) на Fiber Reconciler (React 16+). Fiber — это переработанная внутренняя архитектура, где каждый компонент представлен отдельной единицей работы (fiber-узлом).

Каждый fiber-узел содержит:

  • Тип компонента и пропсы
  • Ссылки на родительский, дочерний и соседний узлы
  • Информацию о состоянии и эффектах
  • Приоритет обновления — ключевое поле для Concurrent Mode

Уровни приоритетов

Scheduler назначает обновлениям один из нескольких уровней приоритета:

Приоритет Примеры Таймаут
Immediate Синхронные обновления, критичные для UX 0 мс
UserBlocking Клики, ввод с клавиатуры ~250 мс
Normal Обновления данных, fetch ~5 с
Low Аналитика, логирование ~10 с
Idle Фоновые задачи бесконечно
import { flushSync } from 'react-dom';
import { startTransition } from 'react';

// UserBlocking приоритет — React обработает это в первую очередь
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  // Срочное обновление: немедленно показать введённый символ
  setInputValue(e.target.value);

  // Normal/Low приоритет — React может отложить это обновление
  startTransition(() => {
    setSearchResults(filterItems(e.target.value));
  });
};

Работа планировщика

Scheduler использует кооперативную многозадачность: он делит работу на небольшие единицы и периодически «спрашивает» браузер, нет ли более важных задач. Для этого применяется MessageChannel (или setTimeout как запасной вариант) — оба механизма позволяют уступить управление браузеру между порциями работы.

Фрейм браузера (16.67 мс при 60 FPS):
┌─────────────────────────────────────────────────────┐
│ Ввод пользователя  │ React работа (5 мс) │ Рисование │
│    (1 мс)          │ → проверяет время   │   (3 мс)  │
└─────────────────────────────────────────────────────┘
                          ↓
              Если > deadline — прерваться,
              передать управление браузеру

Прерывание рендера (Interruptible Rendering)

До Concurrent Mode React не мог остановиться на полпути при обновлении — это называлось «blocking rendering». С Concurrent Mode React получил возможность прерывать, откладывать и отменять незавершённые обновления.

Как работает прерывание

Когда React обрабатывает дерево компонентов в конкурентном режиме:

  1. Он выполняет работу небольшими «чанками» (порциями)
  2. После каждого чанка проверяет, нет ли более приоритетных задач
  3. Если есть — прерывает текущую работу и выполняет приоритетную задачу
  4. Затем либо продолжает прерванную работу, либо начинает заново (если данные изменились)
// Пример: без Concurrent Mode это «заморозило» бы UI
function HeavyComponent({ items }: { items: string[] }) {
  // В синхронном режиме React перерисует все 50 000 элементов без остановки
  // В Concurrent Mode React может прервать этот рендер при срочном вводе
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{expensiveTransform(item)}</li>
      ))}
    </ul>
  );
}

Double-rendering в StrictMode

В режиме разработки StrictMode намеренно вызывает двойной рендер компонентов — это связано с тем, что в Concurrent Mode React может отбрасывать незавершённые рендеры. StrictMode помогает выявить побочные эффекты в render-фазе, которые не должны там находиться.

import { StrictMode } from 'react';

// В development: каждый компонент рендерится дважды
// В production: один раз
const root = createRoot(document.getElementById('root')!);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

Связь с Suspense

Suspense — это механизм, позволяющий компонентам «приостановить» рендеринг до тех пор, пока не будут готовы нужные данные или код. В синхронном React Suspense работал только для code splitting (React.lazy). С Concurrent Mode Suspense стал полноценным инструментом для управления асинхронными состояниями.

Как Suspense интегрируется с Concurrent Mode

Когда компонент «приостанавливается» (throws Promise), React в конкурентном режиме:

  1. Не блокирует весь UI — продолжает рендерить другие части дерева
  2. Показывает ближайший fallback из <Suspense>
  3. Продолжает «в фоне» пытаться завершить приостановленный рендер
  4. Когда данные готовы — мгновенно переключается на результат
import { Suspense, lazy } from 'react';

// Code splitting — компонент загружается лениво
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Загрузка дашборда...</div>}>
      <Dashboard />
    </Suspense>
  );
}

Suspense с серверными данными (React 19+)

// Компонент, который «приостанавливается» при ожидании данных
async function UserProfile({ userId }: { userId: string }) {
  // В React Server Components это работает нативно
  const user = await fetchUser(userId);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userId="123" />
    </Suspense>
  );
}

SuspenseList для координации загрузки

SuspenseList (экспериментальный) позволяет координировать порядок появления нескольких Suspense-компонентов:

import { SuspenseList, Suspense } from 'react';

function FeedPage() {
  return (
    // revealOrder: forwards — показывать по порядку сверху вниз
    // tail: collapsed — показывать только один fallback
    <SuspenseList revealOrder="forwards" tail="collapsed">
      <Suspense fallback={<ArticleSkeleton />}>
        <Article id="1" />
      </Suspense>
      <Suspense fallback={<ArticleSkeleton />}>
        <Article id="2" />
      </Suspense>
      <Suspense fallback={<ArticleSkeleton />}>
        <Article id="3" />
      </Suspense>
    </SuspenseList>
  );
}

Transitions (useTransition, startTransition)

Transitions — это центральная концепция Concurrent Mode для разработчиков. Они позволяют явно пометить обновления состояния как «несрочные», давая React право откладывать их выполнение.

startTransition

startTransition — функция (не хук), которую можно вызвать в любом месте:

import { startTransition, useState } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Срочное обновление — поле ввода должно реагировать мгновенно
    setQuery(e.target.value);

    // Несрочное обновление — результаты поиска можно показать чуть позже
    startTransition(() => {
      setResults(performSearch(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="Поиск..." />
      <ul>
        {results.map((r, i) => <li key={i}>{r}</li>)}
      </ul>
    </div>
  );
}

useTransition

useTransition — хук, который дополнительно предоставляет флаг isPending, позволяющий показывать индикатор загрузки:

import { useTransition, useState } from 'react';

function TabsComponent() {
  const [activeTab, setActiveTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (tab: string) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        {['home', 'about', 'settings'].map(tab => (
          <button
            key={tab}
            onClick={() => switchTab(tab)}
            style={{
              // Визуальная индикация, что переход выполняется
              opacity: isPending ? 0.7 : 1
            }}
          >
            {tab}
          </button>
        ))}
      </nav>

      {/* isPending: true — React рендерит новую вкладку в фоне */}
      {isPending && <span>Загрузка...</span>}

      {/* Текущая вкладка остаётся видимой, пока грузится новая */}
      <TabContent tab={activeTab} />
    </div>
  );
}

Transition с Suspense — избегаем нежелательного fallback

Одно из важнейших преимуществ Transitions — они предотвращают показ fallback при навигации между уже загруженными страницами:

import { useTransition, Suspense, useState } from 'react';

function Router() {
  const [page, setPage] = useState('home');
  const [isPending, startTransition] = useTransition();

  const navigate = (newPage: string) => {
    startTransition(() => {
      setPage(newPage);
    });
  };

  return (
    <div>
      <nav>
        <button onClick={() => navigate('home')}>Главная</button>
        <button onClick={() => navigate('profile')}>Профиль</button>
        <button onClick={() => navigate('settings')}>Настройки</button>
      </nav>

      {/*
        Без Transition: при клике сразу показывается fallback
        С Transition: текущая страница остаётся пока грузится новая,
        fallback показывается только если загрузка занимает слишком долго
      */}
      <Suspense fallback={<PageSkeleton />}>
        {isPending && <div className="loading-indicator">Переход...</div>}
        <Page name={page} />
      </Suspense>
    </div>
  );
}

Практический пример: фильтрация большого списка

import { useState, useTransition, useMemo } from 'react';

// Имитация большого набора данных
const ITEMS = Array.from({ length: 10_000 }, (_, i) => ({
  id: i,
  name: `Элемент ${i}`,
  category: i % 5 === 0 ? 'special' : 'normal',
}));

function HeavyFilteredList() {
  const [filter, setFilter] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Это обновление срочное — поле должно реагировать немедленно
    const value = e.target.value;
    setFilter(value); // Оба setState можно разделить

    // Фильтрация 10 000 элементов — несрочная операция
    startTransition(() => {
      setFilter(value);
    });
  };

  const filteredItems = useMemo(
    () => ITEMS.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    ),
    [filter]
  );

  return (
    <div>
      <input
        value={filter}
        onChange={handleFilterChange}
        placeholder="Фильтр..."
      />

      <p style={{ opacity: isPending ? 0.5 : 1 }}>
        Найдено: {filteredItems.length}
        {isPending && ' (обновляется...)'}
      </p>

      <ul style={{ opacity: isPending ? 0.7 : 1 }}>
        {filteredItems.slice(0, 100).map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Включение Concurrent Mode

Concurrent Mode включается при использовании нового API createRoot (React 18+):

import { createRoot } from 'react-dom/client';
import App from './App';

// Новый API — включает все конкурентные возможности
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

Старый API ReactDOM.render работает в «легасі» режиме без конкурентных возможностей:

// Устаревший API — синхронный рендеринг без Concurrent Mode
// Не использовать в новых проектах!
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

Автоматический батчинг в React 18

Дополнительная возможность Concurrent Mode — автоматический батчинг обновлений состояния. В React 17 батчинг работал только внутри обработчиков событий React. В React 18 батчинг применяется везде: в setTimeout, Promise, нативных обработчиках событий:

// React 17: два отдельных рендера
setTimeout(() => {
  setCount(c => c + 1); // рендер
  setFlag(f => !f);     // ещё один рендер
}, 1000);

// React 18 с createRoot: один рендер (автоматический батчинг)
setTimeout(() => {
  setCount(c => c + 1); // не рендерит
  setFlag(f => !f);     // один рендер в конце
}, 1000);

// Если нужно отключить батчинг в отдельном случае:
import { flushSync } from 'react-dom';

flushSync(() => setCount(c => c + 1)); // сразу рендерит
flushSync(() => setFlag(f => !f));      // сразу рендерит

Когда использовать Concurrent Mode API

Не все обновления нужно оборачивать в startTransition. Используйте правильный инструмент для каждой ситуации:

Ситуация Инструмент
Ввод пользователя (текст, чекбокс) useState — без transition
Фильтрация/сортировка большого списка startTransition / useTransition
Навигация между страницами useTransition
Отложенное обновление производного значения useDeferredValue
Ожидание загрузки данных/кода Suspense
Начальная загрузка страницы Suspense + useTransition
// ✅ Правильно: transition для несрочных обновлений
const [isPending, startTransition] = useTransition();

const handleSearch = (query: string) => {
  setQuery(query);                          // срочно: поле ввода
  startTransition(() => setResults(...));   // несрочно: результаты
};

// ❌ Неправильно: transition для срочных UI-обновлений
startTransition(() => {
  setInputValue(e.target.value); // поле ввода должно реагировать мгновенно!
});

Заключение

Concurrent Mode — это фундаментальное изменение архитектуры React, которое открывает новые возможности для создания отзывчивых интерфейсов:

  • Планировщик (Scheduler) распределяет работу по приоритетам и позволяет React «уступать» браузеру при необходимости
  • Прерываемый рендеринг означает, что сложные обновления не блокируют UI
  • Suspense в конкурентном режиме позволяет элегантно управлять асинхронными состояниями
  • Transitions дают разработчикам явный контроль над приоритетами обновлений
  • Автоматический батчинг сокращает количество лишних рендеров

Начиная с React 18, все эти возможности доступны «из коробки» при использовании createRoot. Ключ к эффективному применению — понимание, какие обновления срочные (ввод пользователя), а какие можно отложить (вычисление результатов, навигация).

Для углублённого изучения архитектуры React и производительных паттернов рекомендую курс по React на PurpleSchool.

Стрелочка влевоCSS Modules в React

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactYup - валидация схемZod - валидация с TypeScriptXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLКак использовать элементы в ReactЧто такое React.js и как его использоватьКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactFragment в React: группировка элементов без лишних узлов DOMПорталы в React: рендер компонентов вне иерархии DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий