Moderní tvorba webových aplikací
O webu

Microtask queue v JavaScriptu

Jak funguje microtask queue, event loop a v jakém pořadí se spouští asynchronní kód v JavaScriptu.

17 minut

JavaScript je jednoduchý. Až na asynchronní kód. Ten je občas záhadný. Zejména pořadí, v jakém se jednotlivé části asynchronního kódu spouštějí.

Klíčem k pochopení je znalost event loopu a rozdíl mezi task queue (někdy též macro task queue) a microtask queue.

Diagram event loopu s microtask a task queue

Event loop

JavaScript v prohlížeči běží na hlavním vlákně, kde v daném okamžiku může probíhat jen jedna operace. I když existují Web Workers pro práci na pozadí, hlavní vlákno zůstává jednovláknové.

Event loop je mechanismus, který umožňuje asynchronní chování JavaScriptu na hlavním vlákně. Neustále kontroluje, zda je call stack (zásobník volání) prázdný, a pokud ano, vezme další úlohu z fronty a provede ji.

Důležité je, že existují dva typy front:

  • Task queue (macro task queue) – pro běžné asynchronní operace
  • Microtask queue – pro prioritní operace, které se mají provést co nejdříve

Microtask queue

Microtask queue je prioritní fronta pro operace, které mají být provedeny hned po dokončení aktuálně běžícího skriptu, ale ještě před tím, než prohlížeč provede další rendering nebo zpracuje další úlohu z task queue.

Co patří do microtask queue?

Následující operace vytváří microtasky:

  • Promise.then(), Promise.catch(), Promise.finally()
  • queueMicrotask()
  • MutationObserver callbacky
  • async/await (interně používá Promises)

Co patří do task queue?

Běžné asynchronní operace vytváří tasky (macro tasky):

  • setTimeout()setInterval()
  • setImmediate() (Node.js)
  • I/O operace
  • UI rendering
  • Uživatelské události (click, scroll, …)

Pořadí vykonávání

Event loop funguje následovně:

  1. Provede se aktuální synchronní kód (call stack)
  2. Když je call stack prázdný, zpracují se všechny microtasky z microtask queue
  3. Prohlížeč může provést rendering
  4. Zpracuje se jeden task z task queue
  5. Celý cyklus se opakuje

Klíčové je, že microtasky mají prioritu. Pokud se během zpracování microtasku přidá další microtask, zpracuje se ještě před tím, než se prohlížeč dostane k dalšímu tasku.

Interaktivní visualisace

Následující demo ukazuje, jak event loop zpracovává synchronní kód, microtasky a tasky krok za krokem:

Připraveno – klikněte na „Spustit” nebo „Krok”
Call Stack
Microtask Queue
Task Queue

Demo ukazuje typický průběh zpracování kódu s setTimeout, PromisequeueMicrotask. Všimněte si, že microtasky se vždy zpracují před tasky!

Praktický příklad

Následující kód ilustruje pořadí vykonávání:

console.log('1: synchronní start');

setTimeout(() => {
  console.log('2: setTimeout (macro task)');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('3: Promise 1 (microtask)');
  })
  .then(() => {
    console.log('4: Promise 2 (microtask)');
  });

queueMicrotask(() => {
  console.log('5: queueMicrotask (microtask)');
});

console.log('6: synchronní konec');

Výstup bude:

1: synchronní start
6: synchronní konec
3: Promise 1 (microtask)
5: queueMicrotask (microtask)
4: Promise 2 (microtask)
2: setTimeout (macro task)

Vysvětlení pořadí

  1. Nejdříve se provede veškerý synchronní kód (řádky 1 a 6)

  2. Pak se zpracují všechny microtasky v pořadí, v jakém byly přidány (řádky 3, 5, 4)

  3. Nakonec se zpracuje macro tasksetTimeout (řádek 2)

Async/await a microtasky

Funkce označené jako async vždy vrací Promise. Klíčové slovo await pozastaví vykonávání funkce a pokračování funkce se zařadí jako microtask.

async function asyncFunkce() {
  console.log('1: start async funkce');

  await Promise.resolve();

  console.log('2: po await (microtask)');
}

console.log('3: před voláním');
asyncFunkce();
console.log('4: po volání');

Výstup:

3: před voláním
1: start async funkce
4: po volání
2: po await (microtask)

Kód po await se chová jako callback v .then() – je zařazen do microtask queue.

queueMicrotask()

API queueMicrotask() umožňuje explicitně přidat callback do microtask queue:

queueMicrotask(() => {
  console.log('Tento kód se spustí jako microtask');
});

Je to čistší a efektivnější alternativaPromise.resolve().then(...). Na rozdíl od Promise nevytváří zbytečný Promise objekt – jde přímo k věci.

// Starý způsob - vytváří Promise objekt
Promise.resolve().then(() => {
  console.log('Microtask přes Promise');
});

// Nový způsob - přímé zařazení do fronty
queueMicrotask(() => {
  console.log('Microtask přímo');
});

Reálné použití queueMicrotask()

Batch aktualisace DOM

Seskupení více DOM operací do jedné, aby se stránka překreslila jen jednou:

let updatesPending = false;
const updates = [];

function scheduleUpdate(element, value) {
  updates.push({ element, value });

  if (!updatesPending) {
    updatesPending = true;
    queueMicrotask(() => {
      // Provede všechny aktualisace najednou
      updates.forEach(({ element, value }) => {
        element.textContent = value;
      });
      updates.length = 0;
      updatesPending = false;
    });
  }
}

// Použití - všechny tři aktualisace se provedou najednou
scheduleUpdate(div1, 'hodnota 1');
scheduleUpdate(div2, 'hodnota 2');
scheduleUpdate(div3, 'hodnota 3');

Zpracování chyb mimo try/catch

Oddělení error handlingu od synchronního kódu:

function asyncOperation(data) {
  if (!data) {
    queueMicrotask(() => {
      throw new Error('Data chybí');
    });
    return;
  }

  // Zpracování dat...
}

// Chyba se hodí až v microtasku,
// takže try/catch zde nechytí nic
try {
  asyncOperation(null);
} catch (e) {
  console.log('Toto se nikdy nespustí');
}

// Místo toho použij:
window.addEventListener('error', (e) => {
  console.log('Chyba zachycena:', e.message);
});

Plugin/Hook systém

Umožnění pluginům reagovat na události v dalším microtasku:

class EventSystem {
  constructor() {
    this.hooks = [];
  }

  registerHook(fn) {
    this.hooks.push(fn);
  }

  trigger(data) {
    // Synchronní zpracování
    this.processData(data);

    // Hooks se spustí až po dokončení
    queueMicrotask(() => {
      this.hooks.forEach(hook => hook(data));
    });
  }

  processData(data) {
    console.log('Zpracování:', data);
  }
}

const events = new EventSystem();
events.registerHook(data => console.log('Hook 1:', data));
events.registerHook(data => console.log('Hook 2:', data));

events.trigger('test');
// Výstup:
// Zpracování: test
// Hook 1: test
// Hook 2: test

Více informací: MDN – queueMicrotask()

MutationObserver

MutationObserver slouží k pozorování změn v DOM stromu. Jeho callbacky se spouštějí jako microtasky:

const observer = new MutationObserver(() => {
  console.log('DOM se změnil (microtask)');
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

document.body.appendChild(document.createElement('div'));
console.log('Prvek přidán (synchronní)');

Výstup:

Prvek přidán (synchronní)
DOM se změnil (microtask)

Pozor na nekonečnou smyčku

Protože se všechny microtasky zpracovávají před dalším taskem, může dojít k zablokování event loopu:

function pridejMicrotask() {
  queueMicrotask(() => {
    console.log('Microtask');
    pridejMicrotask(); // Přidává další microtask
  });
}

pridejMicrotask();
setTimeout(() => {
  console.log('Tento kód se nikdy nespustí!');
}, 0);

V tomto případě se setTimeout callback nikdy nespustí, protože fronta microtasků se neustále doplňuje.

Praktické využití

Zajištění konsistentního stavu

Microtasky se hodí, když potřebujete provést dokončovací logiku po synchronním kódu, ale ještě před renderingem:

// Nastavíme více hodnot synchronně
element.dataset.loading = 'true';
element.textContent = '';

// V microtasku zajistíme konsistentní stav
// před tím, než prohlížeč vykreslí změny
queueMicrotask(() => {
  const data = cache.get('key');
  if (data) {
    element.textContent = data;
    element.dataset.loading = 'false';
  }
});

Debouncing pomocí microtasků

Občas je vhodné seskupit více operací do jedné. Například při sledování změn stavu v reactive frameworku:

class StateManager {
  constructor() {
    this.state = {};
    this.listeners = [];
    this.updatePending = false;
    this.changes = [];
  }

  setState(key, value) {
    const oldValue = this.state[key];
    this.state[key] = value;

    // Uložit změnu
    this.changes.push({ key, oldValue, newValue: value });

    // Naplánovat batch aktualisaci
    if (!this.updatePending) {
      this.updatePending = true;
      queueMicrotask(() => {
        this.notifyListeners(this.changes);
        this.changes = [];
        this.updatePending = false;
      });
    }
  }

  notifyListeners(changes) {
    console.log('Notifikace o změnách:', changes);
    this.listeners.forEach(listener => listener(changes));
  }

  subscribe(listener) {
    this.listeners.push(listener);
  }
}

// Použití
const state = new StateManager();

state.subscribe(changes => {
  console.log(`Provedeno ${changes.length} změn najednou`);
});

// Všechny tři změny se zpracují v jednom microtasku
state.setState('name', 'Jan');
state.setState('age', 30);
state.setState('city', 'Praha');

console.log('Změny naplánované, ale ještě neprovedené');

// Výstup:
// Změny naplánované, ale ještě neprovedené
// Notifikace o změnách: [
//   { key: 'name', oldValue: undefined, newValue: 'Jan' },
//   { key: 'age', oldValue: undefined, newValue: 30 },
//   { key: 'city', oldValue: undefined, newValue: 'Praha' }
// ]
// Provedeno 3 změn najednou

Rozdíly mezi prostředími

Implementace event loopu se mírně liší mezi prohlížečem a Node.js:

  • Prohlížeč – zpracovává rendering mezi tasky
  • Node.js – má více fází event loopu (timers, I/O callbacks, idle, poll, check, close callbacks)

V Node.js existuje také process.nextTick(), který má ještě vyšší prioritu než microtasky.

Debugování a visualisace

Pro pochopení pořadí vykonávání lze použít:

  • Chrome DevTools – Performance tab zobrazuje tasky a microtasky
  • Loupe – visualisační nástroj pro event loop (latentflip.com/loupe)
  • Console logy – nejjednodušší způsob sledování pořadí

Závěr

  • Event loop je srdce asynchronního JavaScriptu

  • Microtask queue má prioritu před task queue (macro task queue)

  • Promises, async/await, queueMicrotask()MutationObserver používají microtasky

  • setTimeout, setInterval a události používají tasky

  • Všechny microtasky se zpracují před dalším taskem nebo renderingem

Pochopení microtask queue je klíčové pro psaní správného asynchronního kódu a debugování neočekávaného chování.

Odkazy jinam

Novinky e-mailem

Když budu mít něco opravdu zajímavého, můžu vám to poslat e-mailem

Přidej se k 500+ čtenářům
Jen kvalitní obsah
Žádný spam

Web jecas.cz píše Bohumil Jahoda, kontakt
Seznam všech článků · Témata
2013–2026