← Назад к вопросам

Почему существуют отдельные очереди для 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)
  • MutationObserver
  • process.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
}

Почему это работает:

  1. Все 1000 промисов добавляются в очередь микротасков
  2. Event Loop выполняет все микротаски ДО рендеринга
  3. Браузер батчит все DOM изменения
  4. Один 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
ПримерыsetTimeoutPromise.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();
  });
});

Вывод

Отдельные очереди нужны, чтобы:

  1. Микротаски были гарантированно быстрыми — высокий приоритет
  2. Батчить обновления — один рендер вместо многих
  3. Контролировать приоритеты — UI важнее фона
  4. Дать возможность браузеру отрисовать — между макротасками

Правило большого пальца:

  • Используй микротаски для синхронизации и батчинга
  • Используй макротаски для отложенной работы
  • Если что-то должно выполниться БЫСТРО — микротаск
  • Если что-то может ПОДОЖДАТЬ — макротаск