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

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

Голодание микрозадач, длинные задачи и LoAF

Суть Паттерн голодания, как обнаружить его в продакшене с помощью PerformanceLongTaskTiming и LoAF, и scheduler.yield() / scheduler.postTask() как лечение.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

Пользователь сообщает, что страница «полностью зависла» на несколько секунд. DevTools Performance показывает один непрерывный жёлтый скриптовый прямоугольник. Ни один вызов функции не доминирует в flame chart — работа продолжает перепланировать себя как микрозадачи, и цикл никогда не вырывается.

Паттерн отказа: голодание микрозадач

Микрозадачи дренируются до пустой между задачами. Если микрозадача планирует ещё одну микрозадачу до возврата, цикл никогда не вырывается из microtask checkpoint — нет рендеринга, нет ввода, нет дальнейших задач. Патологический пример:

function loop() {
  Promise.resolve().then(loop);  // планирует себя как микрозадачу
}
loop();

Это вешает страницу бесконечно: очередь микрозадач никогда не пуста, шаг 4 (рендеринг) никогда не выполняется, пользователь видит замороженную вкладку. DevTools Performance показывает это как один непрерывный жёлтый скриптовый прямоугольник. Тот же паттерн встречается в продакшене как случайная рекурсия через async-цепочки: .then, который переподписывается на тот же observable, роутер-мидлвар, который ре-резолвится при каждом изменении, библиотека состояния, которая перезапускает реакции внутри реакции.

Обнаружение: записи длинных задач без явной доминирующей функции, скриптовый прямоугольник растёт без границ, INP > 1 с.

Лечение: вставить хотя бы одну уступку уровня задачи (setTimeout, MessageChannel, scheduler.postTask) в цепочку.

Сигналы голодания микрозадач в DevTools
Жёлтый скриптовый прямоугольник
непрерывный, никогда не прерывается
Записи Long Tasks API
длительность растёт без границ
INP
> 1 000 мс
Счётчик кадров
0 fps во время голодания
Лечение
вставить уступку уровня задачи в цепочку
Викторина

Колбэк `MutationObserver` срабатывает синхронно после DOM-мутации. Каким видом работы он ставится в очередь?

Long Tasks API и Long Animation Frames

PerformanceLongTaskTiming. Появился в 2017, вызывает запись PerformanceObserver для любой задачи, превышающей 50 мс на главном потоке. Каждая запись включает время начала, длительность и массив attribution с исходным контекстом просмотра. Ограничение: attribution редко указывает на конкретную функцию; он говорит «что-то внутри iframe A заняло 230 мс» — полезно для SLO-дашбордов, менее полезно для поиска корневой причины. Используйте Long Tasks API для подсчёта длинных задач за сессию, а не для их отладки.

PerformanceLongAnimationFrameTiming (LoAF). Появился в 2023–2024 в Chromium. Срабатывает для любого кадра, время рендеринга которого превышает 50 мс. Запись LoAF включает атрибуцию по скриптам: массив того, какие скрипты выполнялись в кадре, что они делали (event-handler, classic-script, module-script, user-callback), сколько каждый занял. Это именно тот продакшен-диагностический инструмент, которым Long Tasks должны были быть с самого начала. Совместно с INP: когда INP регрессирует, запрашивайте записи LoAF из той же сессии, находите виновный кадр, получайте атрибуцию скриптов, деплойте фикс. Полный конвейер: от воспринимаемой пользователем метрики (INP) до конкретного скрипта (LoAF) до конкретной функции (sourcemap по URL скрипта).

Найди ошибку
log
[Long Task] 312 мс
attribution: same-origin
startTime: 2456.3
duration: 312.4

[Long Task] 287 мс
attribution: same-origin
startTime: 5102.7
duration: 287.1

[INP candidate] 410 мс
type: pointerdown -> click -> next paint
startTime: 2401.0
processingStart: 2456.3
processingEnd: 2768.7
presentationTime: 2811.0
attribution: handleSearch (search.js:142)

Поле поиска показывает INP 410 мс, и лог длинных задач указывает на handleSearch. Обработчик делает: валидация запроса → вызов setSearchTerm (Redux) → запуск дебаунсированного fetch → ожидание результатов. Где 312 мс?

scheduler.yield() и Scheduler API

Что делает scheduler.yield(). Scheduler API (Chrome 115+, частично в Edge, Firefox/Safari за флагом на конец 2025) даёт платформе первоклассный примитив уступки. await scheduler.yield() приостанавливает текущую задачу, дренирует очередь микрозадач, позволяет ввоту и рендерингу выполниться, и возобновляет приостановленную задачу как следующую задачу — с приоритетом, который предотвращает низкоприоритетную работу от вклинивания вперёд. Раньше уступка через setTimeout(0) работала для рендеринга, но теряла позицию в очереди (любая задача, запланированная тем временем, выполнялась первой). С scheduler.yield() можно разбить задачу 200 мс на четыре куска по 50 мс, которые с точки зрения пользователя остаются одной логической операцией.

scheduler.postTask() с приоритетами. Тот же API предоставляет scheduler.postTask(callback, { priority }) с тремя приоритетами: user-blocking (работа ответа на ввод), user-visible (по умолчанию), background (несрочная). Практический паттерн: направлять обработчики ввода через user-blocking, чтобы они прыгали в очереди вперёд фоновой работы вроде сброса аналитики; направлять фоновую аналитику через background, чтобы она уступала срочному. Планировщик имеет доступ к системным сигналам (батарея, тепловой троттлинг, видимость страницы), которых нет у JS-кода.

Уступка длинной задачи без потери логической непрерывности

1/3
Какой RFC?

Какая спецификация определяет event loop, очереди задач, очередь микрозадач и шаги рендеринга пошагово?

Спроектируй

Спроектируйте конвейер ввода для поля поиска, фильтрующего 50 000 клиентских элементов и обязанного держать INP ниже 200 мс p75 на среднем Android.

  • Бюджет кадра: ~10 мс после накладных расходов браузера.
  • Цель INP: ≤200 мс p75.
  • Нет длинных задач > 50 мс на главном потоке во время печати.
  • Результаты фильтрации должны обновляться видимо в пределах 200 мс от последнего нажатия клавиши.
  • Внеэкранные результаты могут рендериться лениво, но экранные должны быть корректными.
  • Поддержка браузеров: Chrome, Safari, Firefox (без проблем с воркер-фолбэком).
Викторина

LoAF (PerformanceLongAnimationFrameTiming) отличается от PerformanceLongTaskTiming в ключевом аспекте. В каком?

Вспомните перед уходом
  1. 01
    Поле поиска показывает INP 410 мс. LoAF сообщает о скрипте длительностью 312 мс, атрибутированном handleSearch. Проследите путь от телеметрии до фикса.
  2. 02
    В чём разница между scheduler.yield() и await Promise.resolve() как механизмами уступки?
  3. 03
    Опишите паттерн голодания микрозадач и приведите один продакшен-сценарий, где он появляется случайно.
Итог

Голодание микрозадач возникает когда микрозадача ставит в очередь другую микрозадачу до возврата — цикл застрял в microtask checkpoint и никогда не достигает шагов рендеринга или ввода. В DevTools это проявляется как один непрерывный жёлтый прямоугольник, а в продакшене — как INP > 1 с. Long Tasks API (PerformanceLongTaskTiming, 2017) считает задачи, превышающие 50 мс, но даёт только атрибуцию контекста просмотра. LoAF (PerformanceLongAnimationFrameTiming, 2023) срабатывает на кадр и даёт атрибуцию по скриптам, что делает его правильным инструментом для поиска корневых причин регрессий INP в продакшене. scheduler.yield() из Scheduler API (Chrome 115+) обеспечивает структурированную уступку уровня задач с сохранением приоритета очереди, а scheduler.postTask() позволяет назначать приоритеты user-blocking, user-visible или background, чтобы планировщик браузера — имеющий доступ к тепловым и батарейным сигналам — принимал лучшие решения, чем любая написанная вручную очередь приоритетов.

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

Trademarks belong to their respective owners. Editorial reference only.