React Concurrent Rendering: как работает, когда помогает и где ломают производительность
Подробный разбор Concurrent Rendering в React: scheduler, transition updates, Suspense, useDeferredValue, production-паттерны и типичные ошибки на интервью.
- Зачем вообще понадобился Concurrent Rendering
- Как это работает внутри React: scheduler и приоритеты
- startTransition и useTransition: отделяем срочное от несрочного
- useDeferredValue: отложенное значение вместо отложенного setState
- Suspense и Concurrent Rendering
- Архитектурная схема для production
- Сравнение: синхронный рендер и concurrent-подход
- Production pitfalls: где чаще всего ломают
- 1. Маркируют transition всё подряд
- 2. Ожидают, что transition решит проблемы commit-phase
- 3. Используют useDeferredValue без профита
- 4. Забывают о стабильности ссылок и мемоизации
- Как измерять эффект, а не верить ощущениям
- Best practices для Concurrent Rendering
- Частые ошибки на собеседовании
- Как отвечать на интервью уверенно
- FAQ
- Concurrent Rendering включается отдельно?
- Нужно ли добавлять startTransition во все фильтры и сортировки?
- Может ли Concurrent Rendering ухудшить UX?
- Concurrent Rendering заменяет оптимизацию компонентов?
- Следующий шаг
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 может:
- Начать рендер низкоприоритетного обновления.
- Получить более срочный event (например, ввод текста).
- Прервать текущую работу.
- Быстро обработать срочную задачу.
- Вернуться к отложенному рендеру.
Если говорить инженерно, 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
На крупных экранах обычно работают три уровня состояния:
- Urgent UI state: input, active tab, hover, фокус.
- Transition state: тяжелые вычисления, вторичные панели, списки.
- 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: оптимизация без магии.
Как измерять эффект, а не верить ощущениям
Минимальный набор наблюдаемости:
- React DevTools Profiler:
- сколько времени занимает render у узлов;
- какие компоненты ререндерятся чаще ожидаемого;
- как меняется картина после
startTransition.
- Пользовательские метрики:
- задержка между
keydownи визуальным обновлением input; - время появления результатов поиска;
- длительность блокирующих задач в main thread.
- Сценарные тесты:
- медленное устройство;
- список 5k-20k элементов;
- одновременный ввод + network activity.
Если измерений нет, решение остается предположением.
Best practices для Concurrent Rendering
- Разделяйте urgent и transition обновления только в горячих user-flow.
- Не используйте transition как универсальную обертку для state.
- Ставьте Suspense boundary вокруг дорогих участков UI.
- Проверяйте, что fallback не ломает пользовательский сценарий.
- Держите провайдеры и крупные контейнеры максимально локальными.
- Стабилизируйте props для тяжелых дочерних компонентов.
- Проверяйте производительность на медленных девайсах, не только на ноутбуке разработчика.
- Закладывайте архитектуру рендера на уровне экрана, а не только компонента.
Частые ошибки на собеседовании
- Путать concurrent rendering с многопоточностью JavaScript.
- Говорить, что React "рендерит параллельно на нескольких ядрах".
- Не уметь объяснить разницу между
startTransitionиuseDeferredValue. - Не связывать concurrent-инструменты с реальными метриками UX.
- Думать, что переход на React 18/19 автоматически решает лаги.
Сильный сигнал для интервьюера: вы объясняете не только API, но и условия, при которых этот API приносит пользу.
Как отвечать на интервью уверенно
Рабочая структура ответа:
- Concurrent Rendering - это приоритизация и прерываемость рендера, а не параллельное выполнение на разных потоках.
- Срочные обновления (ввод, взаимодействие) должны оставаться отзывчивыми.
- Несрочные обновления переносим в transition (
startTransition,useTransition) или используемuseDeferredValue. - Для тяжелых частей UI применяем Suspense boundary.
- Эффект подтверждаем профилированием, а не визуальными ощущениями.
Такой ответ показывает, что вы понимаете системную механику 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
Читайте также
frontend
Suspense в React: как использовать без потери UX и производительности
Практический разбор Suspense в React: fallback, lazy loading, границы Suspense, стриминг, ошибки в продакшене и ответы для технического собеседования.
frontend
System design для frontend разработчика: как мыслить системно, а не только компонентами
Практический разбор system design для frontend разработчика: границы ответственности, данные, производительность, ошибки и сильный ответ на интервью.
frontend
React архитектура больших приложений: как не утонуть в слоях, состояниях и фичах
Практический разбор React архитектуры больших приложений: слои, feature-модули, state management, performance, ошибки и критерии выбора.