Мікротаски vs макротаски — чому JavaScript взагалі знадобилися дві черги?

У попередній картці ми для простоти уявляли, що черга одна. Насправді їх дві. Питання не в тому, чим вони відрізняються. Питання в тому, чому однієї виявилось недостатньо.

TypeScript — чому Promise випереджає setTimeout(0)

console.log('1');
setTimeout(() => console.log('2: macrotask'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('4');

// Виведе: 1, 4, 3, 2

Перше питання, яке виникає майже в усіх: чому Promise виконався раніше за setTimeout(0)? Адже таймер теж уже готовий.

💡 JavaScript розділяє задачі не за швидкістю, а за роллю.

Продовження вже розпочатої роботи. У більшості коду, який ми пишемо (і практично у всьому Playwright), microtask використовується саме для продовження вже розпочатої асинхронної операції:

TypeScript — продовження вже розпочатої роботи

const text = await page.textContent();
// виконається тільки після відповіді браузера
console.log(text);

console.log(text) не нова задача, а продовження тієї самої функції після завершення textContent(). Саме тому це microtask.

Macrotask — починає нову незалежну роботу. setTimeout(() => console.log('cleanup'), 0) — колбек таймера не продовжує жоден await. Це окреме прохання: “коли буде можливість — виконай ще й це”.

Саме через цю різницю ролей JavaScript завжди спершу закінчує весь поточний ланцюжок виконання (усі microtasks), і лише потім переходить до наступної незалежної задачі (macrotask).

💡 Не варто запам’ятовувати: “microtasks виконуються раніше”. Краще запам’ятати причину: JavaScript спочатку завершує вже розпочату роботу, і лише потім починає нову.

А хто всім цим керує? Відповідальність тут теж поділена: V8 відповідає за виконання JavaScript і microtasks, libuv відповідає за event loop і macrotasks.

  • Microtask queue обробляє V8. Сюди потрапляє продовження після кожного await і кожного .then(). Оскільки майже всі дії Playwright (click, goto, fill, textContent) завершуються через Promise, їх продовження теж опиняється саме тут.
  • Macrotask queue координує event loop (libuv). Тут живуть setTimeout, setInterval. page.waitForTimeout() зрештою використовує цей самий механізм таймерів.

(Технічна виноска: є рідкісний випадок queueMicrotask(), який дозволяє явно поставити в microtask queue будь-яку функцію, навіть таку, що нічого не “продовжує” — у 99% Playwright-коду це не зустрічається, тому основне правило вище тримається.)

🗣 Як читати вголос У попередній картці ми для простоти уявляли, що черга одна, але насправді їх дві, і питання не в тому чим вони відрізняються, а в тому чому однієї виявилось недостатньо. JavaScript розділяє задачі не за швидкістю, а за роллю. Microtask використовується для продовження вже розпочатої асинхронної операції — наприклад код після await, який просто продовжує ту саму функцію після того як textContent завершився. Macrotask навпаки починає нову незалежну роботу — колбек таймера не продовжує жоден await, це окреме прохання виконати ще щось коли буде можливість. Саме через цю різницю ролей JavaScript завжди спершу закінчує весь поточний ланцюжок виконання, усі microtasks, і лише потім переходить до наступної незалежної задачі. Не варто запам’ятовувати що мікротаски виконуються раніше як окреме правило — краще запам’ятати причину: JavaScript спочатку завершує вже розпочату роботу, і лише потім починає нову. Відповідальність за це поділена: V8 відповідає за виконання JavaScript і мікротаски, libuv відповідає за event loop і макротаски.