Перейти к содержимому

Microtasks vs Macrotasks

Microtask queue (Promise.then, queueMicrotask, MutationObserver) дренируется полностью после каждого task и перед рендером; незнание этого порядка — источник самых каверзных вопросов на Senior-собесах.

Event Loop — одна итерация
┌─────────────────────────────────────────────────────┐
│  1. Взять ОДИН Task из Macrotask Queue              │
│     (setTimeout / setInterval / I/O / UI event)     │
│                                                     │
│  2. Выполнить Task (Call Stack опустошается)        │
│                                                     │
│  3. Дренировать Microtask Queue ДО ПУСТОТЫ          │
│     ┌────────────────────────────────────┐          │
│     │ Promise .then / .catch / .finally  │          │
│     │ queueMicrotask(fn)                 │          │
│     │ MutationObserver callbacks         │          │
│     │ (новые microtasks → в эту же очередь)│         │
│     └────────────────────────────────────┘          │
│                                                     │
│  4. Render (браузер, если нужна перерисовка)        │
│     rAF callbacks → style → layout → paint          │
│                                                     │
│  5. Перейти к шагу 1                               │
└─────────────────────────────────────────────────────┘

Macrotask queue    Microtask queue
[T1][T2][T3] …     [µ1][µ2][µ3] …
↑                   ↑
один за раз         все до конца, включая добавленные в процессе
// Классический вопрос на собесе — предсказать порядок
console.log('1');
setTimeout(() => console.log('2'), 0); // macrotask
Promise.resolve().then(() => console.log('3')); // microtask
queueMicrotask(() => console.log('4')); // microtask
Promise.resolve()
.then(() => {
console.log('5');
return Promise.resolve(); // +1 microtask-тик (resolve с promise)
})
.then(() => console.log('6'));
console.log('7');
// Порядок: 1 → 7 → 3 → 4 → 5 → 6 → 2
// Объяснение: sync (1,7) → все microtasks (3,4,5,6) → macrotask (2)
// Опасность: бесконечные microtasks = hang (I/O starved)
function infiniteMicro() {
Promise.resolve().then(infiniteMicro); // никогда не выйдет из microtask queue
}
// infiniteMicro(); // ← заморозит браузер/Node.js!
// Безопасная альтернатива — macrotask даёт слот рендеру и I/O
function infiniteMacro() {
setTimeout(infiniteMacro, 0); // yield каждую итерацию
}
// queueMicrotask для батчинга синхронных изменений
let pending = false;
function scheduleFlush() {
if (!pending) {
pending = true;
queueMicrotask(() => {
flush(); // один flush на все изменения в текущем task
pending = false;
});
}
}

Итог: Microtask queue — высокоприоритетная очередь, которая полностью опустошается после каждого macrotask; это гарантирует выполнение Promise-колбэков до следующего setTimeout и до рендера.