← Назад к вопросам
Почему существуют отдельные очереди для micro и macro tasks в Event Loop?
2.0 Middle🔥 281 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Event Loop: Micro Tasks vs Macro Tasks
Это одна из самых запутанных (но важнейших) концепций в JavaScript. Понимание порядка выполнения кода критично для отладки timing issues и оптимизации производительности.
Основная концепция Event Loop
while (eventLoop.waitForTask()) {
// 1. Выполни одну MACRO task
const macroTask = eventLoop.nextMacroTask();
if (macroTask) macroTask.execute();
// 2. Выполни ВСЕ MICRO tasks
while (eventLoop.hasMicrotasks()) {
const microTask = eventLoop.nextMicroTask();
microTask.execute();
}
// 3. Отрисовка (если нужна)
if (isRepaintTime()) {
repaint();
}
}
Micro Tasks (микротаски)
Высокий приоритет, выполняются ПЕРЕД рендерингом:
console.log('Start');
// Promise.then() — микротаск
Promise.resolve()
.then(() => console.log('Micro 1'))
.then(() => console.log('Micro 2'));
// setTimeout() — макротаск
setTimeout(() => console.log('Macro'), 0);
console.log('End');
// РЕЗУЛЬТАТ:
// Start
// End
// Micro 1
// Micro 2
// Macro
Типы Micro Tasks:
Promise.then() / .catch() / .finally()queueMicrotask(callback)MutationObserverprocess.nextTick()(Node.js)
Важно: Все микротаски в очереди выполняются ДО нарисовки!
const start = performance.now();
// Этот код блокирует рендеринг!
for (let i = 0; i < 1000; i++) {
Promise.resolve().then(() => {
// 1000 микротасков
updateDOM();
});
}
// Результат: Frame drop (всё отрисуется одновременно)
Macro Tasks (макротаски)
Низкий приоритет, один за раз, между ними — рендеринг:
console.log('Start');
setTimeout(() => {
console.log('Macro 1');
// Микротаски выполняются ПЕРЕД следующим макротаском
Promise.resolve().then(() => console.log('Micro inside Macro'));
}, 0);
setTimeout(() => console.log('Macro 2'), 0);
console.log('End');
// РЕЗУЛЬТАТ:
// Start
// End
// Macro 1
// Micro inside Macro
// Macro 2
Типы Macro Tasks:
setTimeout()setInterval()setImmediate()(Node.js, не браузер)requestAnimationFrame()(спорно, зависит от браузера)- I/O операции (file reading, network requests)
- UI события (click, scroll)
Почему разные очереди? Причины
1. Производительность микротасков
Микротаски выполняются БЫСТРО и НЕ дают рендериться:
// Плохо: блокирует UI
setTimeout(() => {
for (let i = 0; i < 1000; i++) {
updateDOM(); // 1000 reflows
}
}, 0);
// Хорошо: батчит все обновления
for (let i = 0; i < 1000; i++) {
Promise.resolve().then(() => updateDOM()); // Все в одном reflow
}
Почему это работает:
- Все 1000 промисов добавляются в очередь микротасков
- Event Loop выполняет все микротаски ДО рендеринга
- Браузер батчит все DOM изменения
- Один render вместо 1000
2. Гарантия порядка выполнения
Микротаски нужны для гарантирования FIFO (First In, First Out):
Promise.resolve()
.then(() => console.log('1'))
.then(() => console.log('2'))
.then(() => console.log('3'));
// Гарантировано: 1, 2, 3 (в порядке добавления)
// Микротаски НЕ могут быть прерваны другим макротаском
// В отличие от:
setTimeout(() => console.log('A'), 0);
setTimeout(() => console.log('B'), 0);
// Может быть: A, B или B, A (зависит от браузера, других задач)
3. Разные приоритеты для разных операций
// User interaction (high priority)
button.addEventListener('click', () => {
// Микротаск: обновление UI состояния
Promise.resolve().then(() => updateUI());
});
// Low priority background work
setTimeout(() => {
// Макротаск: может быть отложен
syncWithServer();
}, 0);
// Если пользователь кликнет, микротаск UI выполнится ДО фонового синка
Цикл Event Loop: полная картина
┌─────────────────────────────────────────────┐
│ 1. Выполни ОДН макротаск (setTimeout, I/O) │
│ (или ничего, если очередь пуста) │
└────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 2. Выполни ВСЕ микротаски (Promise.then) │
│ (очередь должна быть пуста) │
└────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 3. Отрисовка (requestAnimationFrame) │
│ (каждые ~16ms для 60fps) │
└────────────────┬────────────────────────────┘
↓
Повтори
Практический пример: почему важно
let state = { count: 0 };
function increment() {
state.count++;
// Обновляем DOM сразу?
renderUI();
}
// Сценарий: пользователь кликает дважды
button.addEventListener('click', () => {
increment();
increment();
});
// Плохо: 2 render вызова
// count: 0 -> 1 -> 2
// Хорошо: батчим через микротаск
function increment() {
state.count++;
Promise.resolve().then(() => renderUI());
}
// count: 0 -> 2 (один render)
Микротаски внутри микротасков: опасность
// ⚠️ ОПАСНОСТЬ: Бесконечный loop
function badRecursion() {
Promise.resolve()
.then(() => {
console.log('Micro');
badRecursion(); // Добавляет ещё микротаск
});
}
badRecursion();
setTimeout(() => console.log('Macro'), 0);
// Результат: Микротаски выполняются бесконечно
// Макротаск никогда не выполнится
// ЗАВИСАНИЕ
Решение:
function goodRecursion(depth = 0) {
if (depth > 1000) return; // Guard
Promise.resolve()
.then(() => {
console.log('Micro');
goodRecursion(depth + 1);
});
}
Таблица: Макро vs Микро
| Параметр | Макро Task | Микро Task |
|---|---|---|
| Примеры | setTimeout | Promise.then |
| Выполнение | Один за раз | Все сразу |
| Перед рендерингом | НЕТ | ДА |
| Приоритет | Низкий | Высокий |
| Может быть отложен | ДА | НЕТ |
| Порядок гарантирован | НЕТ | ДА |
Практические применения
Батчинг обновлений (Micro Task)
class StateManager {
constructor() {
this.updates = [];
this.scheduled = false;
}
setState(key, value) {
this.updates.push({ key, value });
if (!this.scheduled) {
this.scheduled = true;
Promise.resolve().then(() => this.flush());
}
}
flush() {
const changes = this.updates;
this.updates = [];
this.scheduled = false;
// Один батч обновлений
this.applyChanges(changes);
this.render();
}
}
Отложенная работа (Macro Task)
// Не блокируй UI, выполни в фоне
function deferWork(callback) {
setTimeout(callback, 0);
}
// После рендеринга
requestAnimationFrame(() => {
// Кадр отрисовался, теперь тяжёлая работа
deferWork(() => {
processLargeDataset();
});
});
Вывод
Отдельные очереди нужны, чтобы:
- Микротаски были гарантированно быстрыми — высокий приоритет
- Батчить обновления — один рендер вместо многих
- Контролировать приоритеты — UI важнее фона
- Дать возможность браузеру отрисовать — между макротасками
Правило большого пальца:
- Используй микротаски для синхронизации и батчинга
- Используй макротаски для отложенной работы
- Если что-то должно выполниться БЫСТРО — микротаск
- Если что-то может ПОДОЖДАТЬ — макротаск