React
Конкурентный рендеринг: идея
Пользователь печатает в поле поиска, под которым обновляется список из тысяч результатов. Без приоритетов происходит так: каждый символ запускает тяжёлый рендер списка, и пока React его строит, поле ввода заморожено - буквы появляются рывками с задержкой. Раньше единственным выходом были ручные обходы вроде debounce. Конкурентный рендеринг меняет саму модель: React больше не обязан рисовать всё за один непрерывный заход. Он может начать рендер, на полпути отвлечься на срочный ввод, а тяжёлую работу доделать потом. Это не оптимизация поверх старой модели, а другой взгляд на то, что значит 'отрендерить'.
- Поиск с живой выдачей: поле остаётся отзывчивым, пока React в фоне перестраивает большой список результатов
- Фильтры над крупными таблицами и дашбордами: переключение фильтра не подвешивает остальной интерфейс
- Навигация между тяжёлыми страницами: старый экран остаётся интерактивным, пока готовится новый
- Стриминговый серверный рендеринг и Suspense: части страницы прибывают и встраиваются по мере готовности
- Архитектура Fiber (React 16): техническая основа, благодаря которой рендер вообще стало возможно прерывать
Предварительные знания
- Reconciliation: как React сравнивает деревья и вычисляет изменения для DOM
- Понимание, что у браузера один основной поток, который рисует UI и выполняет JavaScript
- Идея приоритета задач: одни дела важнее и должны выполняться раньше других
Долгий путь к прерываемому рендерингу
Идея зрела годами. В React 16 (2017) движок reconciliation переписали на архитектуру Fiber специально ради того, чтобы работу рендера можно было дробить на маленькие единицы и прерывать между ними. Это была инфраструктура без публичного API. Несколько лет команда экспериментировала под именами Async Mode, затем Concurrent Mode, осторожно меняя подход. В React 18 (2022) конкурентность наконец вышла стабильно - не как режим, который надо включать, а как набор возможностей, активируемых конкретными API вроде useTransition и useDeferredValue. К 2026 году, с React 19.2, эта модель лежит в основе и Suspense, и стриминга, и нового компонента Activity.
Проблема блокирующего рендера
До конкурентной модели рендер был синхронным и неделимым. Когда менялось состояние, React начинал строить новое дерево и не отдавал управление браузеру, пока не закончит. Если работа большая - перерисовка списка из тысяч элементов, - основной поток занят целиком, и всё это время браузер не реагирует ни на что: ввод, скролл, анимации замирают. Пользователь видит фриз.
Корень в том, что у браузера один основной поток. Он и выполняет JavaScript, и рисует интерфейс. Пока React синхронно считает большой рендер, этот единственный поток не может обработать нажатие клавиши. Поэтому тяжёлый несрочный рендер напрямую крадёт отзывчивость у срочного ввода.
Исторически проблему обходили вручную: debounce и throttle откладывали обновление, виртуализация резала число элементов, тяжёлые вычисления выносили в Web Worker. Эти приёмы остаются полезными, но они борются со следствием. Конкурентность атакует причину - саму неделимость рендера.
Почему синхронный рендер большого списка замораживает поле ввода?
Рендер можно прервать, возобновить или отбросить
Конкурентная модель снимает главное допущение: рендер больше не обязан быть единым непрерывным заходом. Благодаря архитектуре Fiber React дробит работу на маленькие единицы и между ними сверяется с браузером. Если появилось что-то срочное - нажатие клавиши, - React приостанавливает текущий рендер, отдаёт поток браузеру, обрабатывает срочное, а потом возвращается и доделывает отложенное.
- Приостановить: React останавливает тяжёлый рендер на границе единицы работы и отдаёт поток браузеру
- Возобновить: разобравшись со срочным, React продолжает прерванный рендер с того же места
- Отбросить: если состояние снова изменилось и начатый рендер устарел, React выбрасывает его результат и начинает заново
Способность отбрасывать особенно важна. Пока React в фоне строит список для запроса 'react', пользователь дописывает до 'react hooks'. Старый рендер мгновенно устарел - и React просто отбрасывает его, не показывая на экране ни одного промежуточного кадра. Пользователь никогда не увидит результат для неактуального запроса. Это не задержка показа, а отмена ненужной работы.
Конкурентность - не многопоточность. React по-прежнему живёт в одном основном потоке, а не считает рендер на втором ядре параллельно. 'Конкурентность' здесь - про умное распределение работы во времени: дробить, чередовать, отменять. Параллельного выполнения JavaScript в браузере тут нет.
Пользователь быстро меняет запрос, и React уже начал рендерить результаты для предыдущего, устаревшего запроса. Что он сделает в конкурентной модели?
Срочные и несрочные обновления
Чтобы React понимал, что прерывать, а что доводить немедленно, обновления делятся на два класса. Срочные - те, где задержка сразу ощущается как лаг: ввод текста, клик, переключение чекбокса. Несрочные - те, где небольшая отсрочка приемлема: перерисовка большого списка результатов в ответ на этот ввод. Идея в том, чтобы срочное обновление никогда не ждало завершения несрочного.
- Срочное обновление — Прямая реакция на действие: буква в поле, состояние кнопки. Должно примениться немедленно, иначе интерфейс ощущается тормозящим.
- Несрочное обновление — Производная тяжёлая работа: перестроить список из тысяч строк по новому фильтру. Может подождать пару кадров без вреда для ощущения.
Важно: React не угадывает приоритеты сам. По умолчанию все обновления срочные. Разработчик помечает несрочную работу явно - через специальные API. useTransition оборачивает обновление состояния и говорит 'это можно отложить', а useDeferredValue откладывает производное значение. Оба будут разобраны в следующих уроках. Этот урок даёт модель, на которой они стоят.
Главное, что стоит унести: конкурентность - это модель про время и приоритет, а не про скорость самого вычисления. Тяжёлый рендер не становится быстрее. Он просто перестаёт блокировать срочные действия, потому что React раскладывает работу во времени и при необходимости отменяет устаревшую.
Как React по умолчанию определяет, какое обновление срочное, а какое нет?
Связь с другими темами
Этот урок - ментальная база под весь блок конкурентности:
- useTransition — Помечает обновление как несрочное, чтобы срочный ввод не ждал тяжёлого рендера
- useDeferredValue — Откладывает значение, давая дорогому потомку рендериться с низким приоритетом
- Reconciliation — Процесс, который конкурентность делает прерываемым и приоритизируемым
Итог
- Конкурентный рендеринг делает рендер прерываемым: React может приостановить начатую работу, переключиться на срочную и вернуться к ней позже
- Начатый рендер можно даже полностью отбросить, если его результат устарел - пользователь никогда не увидит промежуточное состояние
- Обновления делятся на срочные (ввод, клики) и несрочные (тяжёлая перерисовка списка от этого ввода)
- Это ментальная модель, а не магия: основа - архитектура Fiber, дробящая работу на прерываемые единицы, стабильна с React 18 (2022)
- Сам по себе рендер не становится параллельным потоком - React по-прежнему в одном основном потоке, но умнее распределяет работу во времени
Связанные уроки
- rc-17-reconciliation — Конкурентный рендеринг переосмысливает процесс reconciliation, делая его прерываемым - поэтому базовая модель reconciliation обязательна
- rc-25-usetransition — useTransition - первый практический API, который помечает обновления как несрочные, опираясь на эту модель
- rc-26-usedeferredvalue — useDeferredValue - второй инструмент конкурентности, откладывающий значение на основе тех же приоритетов