Dominar Event Loop y Microtasks para evitar problemas de rendimiento

event-loop-microtasks-concurrencia

Event Loop, Microtasks y Concurrencia Interna

Tiempo estimado de lectura: 4 min

  • Microtasks tienen prioridad sobre macrotasks y render.
  • Una cola de microtasks que se auto‑agenda puede provocar starvation.
  • En Node hay matices: process.nextTick y setImmediate afectan el orden.
  • Chunking o workers son la forma segura de manejar trabajo pesado.

Introducción

Event Loop, Microtasks y Concurrencia Interna: dominar esto no es solo saber usar async/await. En las primeras líneas: si no entiendes Call Stack, Web/Node APIs, Task Queue y Microtask Queue —y la prioridad que tienen— seguirás viendo servidores congelados y frontends con “jank”.

Resumen rápido (lectores con prisa)

El motor ejecuta macrotasks una por iteración y vacía la cola de microtasks tras cada macrotask. Las microtasks se ejecutan antes que render y macrotasks, y pueden provocar starvation si se auto‑agendan. En Node hay colas adicionales como process.nextTick y diferencias entre setImmediate y setTimeout.

Event Loop, Microtasks y Concurrencia Interna: qué debes dominar

  • Call Stack: donde corre el código síncrono. Si algo ocupa la pila mucho tiempo, todo se bloquea.
  • Web APIs / Node APIs: el entorno (browser, libuv) ejecuta I/O, timers y los devuelve como callbacks.
  • Task Queue (Macrotasks): setTimeout, I/O, eventos; se procesa una macrotask por iteración.
  • Microtask Queue: Promise.then, queueMicrotask, MutationObserver (y en Node process.nextTick con sus matices); se vacía completamente tras cada macrotask.
  • Prioridad: microtasks > macrotasks > render (en browsers).
  • Starvation: microtasks recursivas pueden impedir que el loop avance.
  • Node specifics: setImmediate vs setTimeout, process.nextTick vs Promise.resolve.

Cómo funciona en la práctica (y por qué te importa)

Cada iteración del loop suele seguir este patrón:

  1. Ejecuta la macrotask en curso.
  2. Vacía la microtask queue completamente.
  3. (Browsers) Renderiza si es necesario.
  4. Coge la siguiente macrotask.

Consecuencia directa: si una microtask agenda otra microtask sin parar, el motor se queda en el paso 2 y nunca llega al render ni a otras macrotasks. Resultado: UI congelada o servidor que no responde.

Ejemplo de starvation (no lo hagas en producción):

function starve() {
  Promise.resolve().then(starve);
}
starve();

Node.js: matices que importan

Node tiene fases internas; aquí las diferencias que debes aplicar:

  • process.nextTick vs Promise.resolve

    process.nextTick tiene su propia cola y se procesa antes que la cola de Promises. Úsalo para cleanup urgente, pero evita recursividad: un nextTick recursivo bloquea I/O.

  • setImmediate vs setTimeout(…, 0)

    Dentro de un callback de I/O, setImmediate se ejecuta antes que timers. En el script principal el orden puede ser no determinista. Para tareas post‑I/O preferimos setImmediate.

Ejemplo ilustrativo:

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout-in-io'), 0);
  setImmediate(() => console.log('immediate-in-io'));
});
// Salida consistente: immediate-in-io → timeout-in-io

Fuente: Node docs.

Reglas prácticas y patrones seguros

  1. Microtasks para consistencia, macrotasks para ceder control

    Usa microtasks (queueMicrotask, Promise.then) para garantizar orden lógico inmediato, no para procesar grandes volúmenes.

  2. Chunking: divide trabajo pesado

    Para arrays grandes o computación pesada, trocea la ejecución y programa cada chunk con macrotasks (setTimeout / setImmediate) o usa Web Workers / Worker Threads para off‑main.

  3. Evita process.nextTick sin pensar

    Es potente pero peligroso; documenta su uso y limita su alcance.

  4. Usa herramientas de profiling

    Chrome DevTools Performance para front; clinic.js, --inspect para Node. Busca long tasks y spikes en microtasks.

  5. Prefiere APIs modernas cuando estén disponibles

    scheduler.yield() o requestIdleCallback/requestAnimationFrame son mejores opciones en escenarios específicos (revisar compatibilidad).

Chunking ejemplo:

function processLarge(items, chunk = 200) {
  let i = 0;
  function next() {
    const end = Math.min(i + chunk, items.length);
    for (; i < end; i++) heavy(items[i]);
    if (i < items.length) setTimeout(next, 0); // cede el hilo
  }
  next();
}

Criterio técnico resumido (para tu equipo)

  • Define en code reviews: microtasks solo para cosas <10ms y que requieran orden inmediato.
  • Chunking obligatorio para loops que procesen >1k elementos.
  • Mueve CPU‑bound a workers.
  • En Node: setImmediate post‑I/O, nextTick solo para cleanup crítico.

Conclusión

Dominar Event Loop, Microtasks y Concurrencia Interna no es trivia: es diseño de sistemas. Entender quién tiene prioridad (microtasks) y cómo evitar starvation cambia aplicaciones que “funcionan” por aplicaciones que escalan y no frustran usuarios. Si quieres que tu equipo deje de parchear problemas de rendimiento, empieza por aquí.

FAQ

Respuesta:

Las microtasks (ej. Promise.then) se ejecutan inmediatamente después de la macrotask en curso y antes del render; la cola se vacía por completo. Las macrotasks (ej. setTimeout, I/O) se procesan una por iteración del loop.

Respuesta:

Porque la cola de microtasks se vacía completamente tras cada macrotask: si una microtask agenda otra microtask recurrentemente, el loop nunca avanza a render ni a nuevas macrotasks, congelando la UI o bloqueando I/O.

Respuesta:

process.nextTick se usa para cleanup urgente que debe ejecutarse antes de otras promesas. Evítalo para trabajo repetitivo o no crítico, ya que su cola se procesa antes que la cola de Promises y puede bloquear I/O si se abusa.

Respuesta:

Usar chunking con macrotasks (setTimeout, setImmediate) o delegar a Web Workers / Worker Threads para mover CPU‑bound fuera del hilo principal.

Respuesta:

En frontend, Chrome DevTools → Performance para identificar long tasks. En Node, usar clinic.js y --inspect para perf profiles; buscar spikes y colas saturadas de microtasks.

Respuesta:

Trocea procesamiento grande en chunks (ej. 200–1000 items) y programa la ejecución de cada chunk con macrotasks para ceder el hilo entre ellos. Chunking obligatorio para loops >1k según criterio técnico.

Referencias

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *