awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Приоритетные lanes, time-slicing и useTransition

Суть React 18 присваивает каждому обновлению lane (приоритетный бакет). Time-slicing разбивает работу рендера на слайсы по 5 мс. useTransition и useDeferredValue переводят дорогие обновления в transition-lane, чтобы нажатия клавиш могли их прерывать.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Вы оборачиваете медленную фильтрацию списка в startTransition. Набор текста остаётся мгновенным, список догоняет при наличии свободного времени, а спиннер сигнализирует об ожидании. Без этого каждое нажатие клавиши вызывает блокирующий рендер на 50 мс. Механизм, делающий это возможным, — система приоритетных lanes React.

Приоритетные lanes. React 18 присваивает каждому обновлению lane — приоритетный бакет. Модель lanes (битмаска из 31 lane) позволяет React сначала работать над высокоприоритетными обновлениями и откладывать низкоприоритетные. Важные уровни:

  • Sync / discrete: Клик, нажатие клавиши. React рендерит немедленно и блокирующе, потому что пользователь ждёт именно этого взаимодействия.
  • Default: Обычные обновления состояния без transition-обёртки.
  • Transition: Задаётся через useTransition или useDeferredValue. Используется для обновлений, которых пользователь не ждёт напрямую — фильтрация большого списка, переход на новый вид. Может быть прервано чем-то более срочным.
  • Idle: Самый низкий. Фоновая работа, которую приложение может делать, когда главный поток свободен.

Когда нажатие клавиши приходит в середине transition-рендера, React бросает незавершённый transition-рендер, обрабатывает нажатие с высоким приоритетом и перезапускает transition после.

Уровни приоритета lanes
sync / кликРендерится немедленно, блокирующе — пользователь ждёт
defaultОбычный setState — планируется скоро, не блокирует
transitionstartTransition / useDeferredValue — прерываемый, time-sliced
idleСамый низкий — запускается только когда больше ничего нет

Time-slicing: как фаза render уступает. Длинный синхронный рендер блокирует ввод — именно тот режим отказа, который описывается в теме об event loop. Планировщик React разбивает фазу render на слайсы. После примерно 5 мс работы реконсиляции планировщик проверяет shouldYield(); если true — останавливается, публикует задачу продолжения через MessageChannel (кросс-браузерный способ запланировать задачу без clamp-а) и отдаёт браузеру обработку событий и рендеринг. Задача продолжения возобновляет рендер с сохранённого указателя fiber.

Рендер на 50 мс превращается в десять слайсов по 5 мс, перемежающихся с работой браузера, и ввод остаётся отзывчивым. Это применимо только к конкурентным рендерам — transitions и другим несрочным обновлениям. Обновление с sync-приоритетом (дискретный ввод) всё равно рендерится одним блокирующим проходом, потому что заставить пользователя ждать yield противоречило бы цели.

Битмаска lanes. Lane — это один бит в 31-битном целом числе. Представление приоритета как битмаски позволяет React делать операции над множествами за одну инструкцию CPU: «есть ли pending высокоприоритетные lanes?» — это mask-and-test; «рендерить все lanes на уровне или выше этого приоритета» — сравнение битмасок. Обновления в совместимых lanes батчатся — несколько вызовов setState в одном обработчике события (или, с React 18, через промисы и таймауты через автоматический батчинг) сворачиваются в один рендер. Именно поэтому нельзя прочитать новое состояние синхронно сразу после setState: рендер, применяющий его, ещё не произошёл.

useTransition и useDeferredValue. Это два хука, переводящих работу в transition-lane. useTransition даёт функцию startTransition — обновления состояния внутри неё помечаются низкоприоритетными и прерываемыми, плюс флаг isPending для показа спиннера. Канонический случай: поле поиска, где само значение поля обновляется синхронно (набор текста ощущается мгновенным), а дорогое обновление отфильтрованного списка обёрнуто в startTransition (уступает следующим нажатиям клавиш).

useDeferredValue — та же идея со стороны потребителя: даёт копию значения, «отстающую» от оригинала — React обновляет её с transition-приоритетом, так что компонент, читающий отложенное значение, рендерится только когда есть свободное время. Используйте useTransition, когда вы владеете обновлением состояния; useDeferredValue — когда значение приходит из props или контекста, которыми вы не управляете.

Suspense и приостановка дерева. Lanes тесно взаимодействуют с Suspense. Когда компонент внутри <Suspense> бросает промис (ожидая данные), React приостанавливает рендер этого поддерева и показывает fallback. В concurrent-режиме это позволяет React продолжать рендерить другие части дерева в более приоритетных lanes, пока данные загружаются. Transition-lane делает Suspense неблокирующим: можно обернуть навигацию в startTransition, и React будет показывать старый экран (с индикатором pending), пока новый экран данных ещё загружается, а не показывать fallback немедленно.

Не давать дорогому фильтрованному списку блокировать ввод

1/3
Закончи аналогию

React 18 присваивает каждому обновлению приоритетный бакет, чтобы рендерить срочную работу первой и откладывать остальное. Нажатие клавиши попадает в синхронный бакет; дорогая фильтрация списка, обёрнутая в startTransition, — в низкий прерываемый бакет. Как React называет эти приоритетные бакеты?

Викторина

Вы обернули дорогое обновление списка в `startTransition`. В середине этого рендера приходит нажатие клавиши. Что делает React?

Викторина

Какой вид обновления React рендерит одним блокирующим проходом без time-slicing?

Посчитай

Планировщик React делает примерно 5 мс работы рендера за слайс. Бюджет кадра при 60 fps — 16,67 мс. Примерно сколько слайсов рендера React помещается в один кадр при ~6 мс накладных расходов браузера?

слайса
Вспомните перед уходом
  1. 01
    Почему событие клика рендерится синхронно и блокирующе, а обновление через startTransition — нет?
  2. 02
    Как React уступает браузеру между слайсами рендера?
  3. 03
    В чём разница между useTransition и useDeferredValue?
Итог

Система lanes React 18 присваивает каждому обновлению приоритетный бакет. Дискретный ввод (клики, нажатия) рендерится в sync-lane — один блокирующий проход без yield. Обычные обновления состояния используют default-lane. Работа, обёрнутая в startTransition или читаемая через useDeferredValue, работает в transition-lane — прерываемая и time-sliced. Планировщик разбивает transition-рендеры на слайсы по ~5 мс, публикует продолжение через MessageChannel между слайсами и возобновляет с сохранённого указателя fiber. Именно поэтому рендер списка на 50 мс не роняет кадры: он превращается в десять слайсов по 5 мс, перемежающихся с перерисовками браузера и обработкой ввода. Когда нажатие клавиши приходит посреди transition, React бросает незавершённый рендер, обрабатывает нажатие с sync-приоритетом и перезапускает transition с нуля.

Связанные уроки
встречается в143
Продолжить восхождение ↑Bailout, мемоизация и tearing
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.