Desentrañando el Event Loop: más allá de los timers y promesas

El Event Loop es el corazón de la ejecución asíncrona en JavaScript. Aunque a primera vista parece magia, en realidad sigue un conjunto de reglas bien definidas que nos permiten escribir código no bloqueante sin complicarnos la vida. En este artículo vamos a desmenuzar su funcionamiento, centrándonos en la interacción entre la call stack, la macro‑task queue y la micro‑task queue. Al final tendrás una visión clara para depurar problemas de timing y optimizar tu código.
1. La pila de llamadas (call stack)
Todo comienza con la call stack. Cada vez que llamas a una función, el motor de JavaScript la empuja a la pila. Cuando la función termina, se desapila. Mientras la pila no esté vacía, el Event Loop no hace nada más.
2. Cola de macro‑tareas (macrotask queue)
Las macro‑tareas son unidades de trabajo que se programan para ejecutarse en el futuro: setTimeout, setInterval, setImmediate (Node) o eventos del DOM. Cada vez que el Event Loop termina de vaciar la pila, saca la siguiente macro‑tarea de la cola y la ejecuta.
3. Cola de micro‑tareas (microtask queue)
Las micro‑tareas son más ligeras y tienen prioridad sobre las macro‑tareas. Incluyen Promise callbacks (.then, .catch), queueMicrotask y los callbacks de MutationObserver. Después de ejecutar una macro‑tarea, el Event Loop vacía todas las micro‑tareas pendientes antes de pasar a la siguiente macro‑tarea.
4. El ciclo completo paso a paso
Veamos un ejemplo que combina ambos tipos de colas y muestra el orden de ejecución.
<script>
console.log('Inicio');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
});
queueMicrotask(() => {
console.log('Microtask directa');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('Fin');
</script>
¿Cuál es el resultado en la consola?
- Inicio – se ejecuta inmediatamente al entrar en la pila.
- Fin – aún dentro de la misma pila, antes de que el Event Loop haga nada.
- Promise 1 – es una micro‑tarea, se ejecuta antes de cualquier
setTimeout. - Microtask directa – también micro‑tarea, se ejecuta justo después de
Promise 1. - Timeout 1 – ahora la pila está vacía, el Event Loop toma la primera macro‑tarea.
- Timeout 2 – segunda macro‑tarea.
El orden final es: Inicio → Fin → Promise 1 → Microtask directa → Timeout 1 → Timeout 2. Este comportamiento explica por qué las promesas parecen «más rápidas» que los setTimeout con retardo 0.
5. Async/Await bajo el capó
Cuando utilizas async/await, el motor transpila tu código a promesas. Cada await pausa la ejecución, coloca la continuación en la cola de micro‑tareas y permite que el Event Loop continúe procesando otras tareas. Veamos una variante sencilla:
async function ejemplo() {
console.log('Antes del await');
await Promise.resolve();
console.log('Después del await');
}
console.log('Inicio script');
ejemplo();
console.log('Fin script');
Salida: Inicio script → Antes del await → Fin script → Después del await. El await no bloquea la pila; simplemente agenda la continuación como micro‑tarea.
6. Por qué entender el Event Loop es útil
- Depuración de race conditions: si una operación depende del orden de ejecución, saber qué se ejecuta primero evita bugs difíciles.
- Optimización de renderizado: colocar código pesado en micro‑tareas puede bloquear la actualización del UI; a veces es mejor usar
setTimeoutpara ceder al navegador. - Gestión de recursos en Node.js: el mismo modelo se aplica en el servidor, donde la cola de macro‑tareas incluye I/O, timers y callbacks de
process.nextTick(una micro‑tarea especial).
7. Buenas prácticas rápidas
- Usa
Promisepara lógica asíncrona siempre que sea posible; las micro‑tareas son más predecibles. - Evita bloques de código muy pesados dentro de una sola macro‑tarea; divide la carga con
setTimeout(..., 0)si necesitas liberar el hilo. - En Node, reserva
process.nextTickpara inicializaciones críticas; de lo contrario, prefieresetImmediatepara evitar starvations.
Dominar el Event Loop no solo te ayuda a escribir código más limpio, sino que también te da la capacidad de razonar sobre el rendimiento y la responsividad de tus aplicaciones. La próxima vez que veas un Promise o un setTimeout, recuerda en qué cola están y cómo el Event Loop decide su orden de ejecución.