Микрозадачи и макрозадачи

Микрозадачи и макрозадачи #

JavaScript выполняет код в одном потоке, а отложенную работу ставит в очереди. Две важные категории — микрозадачи (microtasks) и макрозадачи (macrotasks, task). Понимание порядка объясняет, почему then срабатывает раньше setTimeout, и помогает избегать гонок в UI.


Один цикл за раз #

Пока выполняется синхронный код, очереди не продвигаются. После текущего «куска» движок:

  1. Обрабатывает все микрозадачи из очереди (до опустошения).
  2. Берёт одну макрозадачу.
  3. Снова обрабатывает все микрозадачи.
  4. И так далее — модель event loop.

Что куда попадает #

ТипПримеры
Микрозадачаколбэки .then / .catch / .finally, queueMicrotask, await (продолжение async-функции)
МакрозадачаsetTimeout, setInterval, события DOM, часть I/O в Node

requestAnimationFrame в браузере — отдельная фаза перед отрисовкой; не путайте с микрозадачами, но помните: он ближе к кадру, чем setTimeout(0).


Классический пример порядка #

console.log('1 sync')

setTimeout(() => console.log('2 macrotask'), 0)

Promise.resolve().then(() => console.log('3 microtask'))

console.log('4 sync')

// 1 sync → 4 sync → 3 microtask → 2 macrotask

Синхронный код завершается, затем опустошается очередь микрозадач (промис), и только потом — таймер.


Цепочка микрозадач #

Каждый .then добавляет новую микрозадачу; они выполняются подряд до следующей макрозадачи:

Promise.resolve()
  .then(() => console.log('A'))
  .then(() => console.log('B'))

setTimeout(() => console.log('C'), 0)

// A → B → C

Длинные цепочки промисов без await между кадрами могут отложить отрисовку — в тяжёлых UI разбивайте работу на макрозадачи (setTimeout, requestAnimationFrame) или чанки.


async/await и микрозадачи #

await приостанавливает async-функцию; продолжение после разрешения промиса — микрозадача:

async function demo() {
  console.log('start')
  await null
  console.log('after await')
}

demo()
console.log('sync end')

// start → sync end → after await

queueMicrotask #

Явная постановка микрозадачи:

queueMicrotask(() => {
  console.log('обновить счётчик без setTimeout(0)')
})

Предпочтительнее setTimeout(fn, 0), когда нужно выполнить код сразу после текущего синхронного блока, но до следующей макрозадачи.


Практический смысл #

  • Обновление DOM после промиса — выполнится до таймеров с нулевой задержкой; тесты на setTimeout(0) могут быть хрупкими.
  • Бесконечные микрозадачи (then → then → … без пауз) заблокируют обработку кликов и таймеров — starvation макрозадач.
  • Ошибка в микрозадаче без .catch — unhandledrejection; в синхронном коде — обычный try/catch не поймает.
Promise.resolve().then(() => {
  throw new Error('в микрозадаче')
})
// try/catch вокруг этого блока не сработает

Итог #

  • После синхронного кода движок сначала выполняет все микрозадачи, затем одну макрозадачу.
  • Промисы и await — микрозадачи; setTimeout — макрозадача.
  • Порядок sync → microtasks → macrotask объясняет большинство «загадочных» логов в консоли.
  • Не создавайте бесконечный поток микрозадач; для уступки потоку используйте макрозадачи или разбиение работы.

См. также #

Часто спрашивают

Что выбрать — then или async/await?
async/await обычно читается проще для последовательного кода; then удобен для коротких цепочек и комбинирования с Promise API. Подробнее
Чем Promise.all отличается от allSettled?
all падает при первой ошибке; allSettled дожидается всех и отдаёт статус каждого результата. Подробнее