Event Loop en JavaScript - Cómo manejar tareas asíncronas de forma real

Lectura de 5 minutos

JavaScript es single-threaded, pero gracias al Event Loop, puede manejar múltiples tareas asíncronas sin bloquear la UI. Entenderlo te permite escribir código más predecible y optimizar la experiencia de usuario.

1. Cómo funciona el Event Loop

El Event Loop coordina la ejecución de tareas en JavaScript:

  1. Stack de ejecución: donde se ejecuta el código síncrono.
  2. Cola de microtasks: promesas (Promise.then), queueMicrotask, MutationObserver.
  3. Cola de macrotasks: setTimeout, setInterval, eventos del DOM.
  4. requestAnimationFrame: cola especial para animaciones, se ejecuta antes del repintado del navegador.

Regla clave:

  • Se ejecuta todo el código del stack principal.
  • Luego se vacía la cola de microtasks.
  • Después se ejecutan los requestAnimationFrame callbacks (si los hay).
  • Después se ejecuta la siguiente macrotask, y el ciclo se repite.

2. Ejemplo básico

console.log("Inicio");

setTimeout(() => console.log("Timeout 0"), 0);

Promise.resolve()
  .then(() => console.log("Promise 1"))
  .then(() => console.log("Promise 2"));

requestAnimationFrame(() => console.log("RAF"));

console.log("Fin");

Salida:

Inicio
Fin
Promise 1
Promise 2
RAF
Timeout 0
  • "Inicio" y "Fin": código síncrono.
  • Promise.then: microtask, se ejecuta antes de cualquier macrotask.
  • requestAnimationFrame: se ejecuta después de microtasks, antes del repintado.
  • setTimeout: macrotask, se ejecuta al final.

3. Ejemplo real: buscador con autocompletado

Imagina un input de búsqueda que hace peticiones a un API mientras el usuario escribe. Queremos que:

  • Cancelar la petición anterior si el usuario sigue escribiendo.
  • Mostrar resultados sin bloquear la UI.
<input id="search" placeholder="Busca algo..." />
<ul id="results"></ul>

<script>
const input = document.getElementById("search");
const results = document.getElementById("results");
let controller;

input.addEventListener("input", async (e) => {
  const query = e.target.value;

  // Cancelar petición anterior si existe
  if (controller) controller.abort();
  controller = new AbortController();

  try {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/todos?q=${query}`,
      { signal: controller.signal } // Asociamos la petición al controller
    );
    const data = await res.json();

    results.innerHTML = "";
    data.slice(0, 5).forEach(item => {
      const li = document.createElement("li");
      li.textContent = item.title;
      results.appendChild(li);
    });
  } catch (err) {
    if (err.name === "AbortError") return; // Petición cancelada
    console.error(err);
  }
});
</script>

Qué pasa según el Event Loop

  1. Cada evento input entra como macrotask.
  2. La llamada a fetch devuelve una Promise inmediatamente.
  3. Cuando el servidor responde, la resolución de esa Promise programa sus .then() callbacks como microtasks.
  4. Si el usuario escribe rápido, la macrotask siguiente cancela la petición anterior con controller.abort().
  5. La UI nunca se bloquea y los resultados aparecen en orden correcto.

Lección: El Event Loop permite que tareas asíncronas y eventos del DOM se mezclen sin bloquear la UI. Combinarlo con AbortController y signal hace que tu app sea mucho más reactiva y eficiente.

4. requestAnimationFrame: optimizando animaciones

Para animaciones suaves, usa requestAnimationFrame en lugar de setTimeout. Se ejecuta justo antes del repintado del navegador (normalmente 60fps):

let position = 0;

function animate() {
  position += 2;
  
  const element = document.getElementById('box');
  element.style.left = position + 'px';
  
  if (position < 500) {
    requestAnimationFrame(animate); // Siguiente frame
  }
}

requestAnimationFrame(animate);

Ventajas de requestAnimationFrame:

  • Se sincroniza con la frecuencia de refresco del monitor.
  • Se pausa cuando la pestaña no está visible (ahorra batería).
  • Mejor rendimiento que setTimeout para animaciones.

5. Resumen visual

Stack principal: código síncrono
        |
        v
Microtasks: Promise.then, queueMicrotask, MutationObserver
        |
        v
requestAnimationFrame: callbacks de animación
        |
        v
Macrotasks: setTimeout, setInterval, input/DOM events

Orden de prioridad:

  1. Stack principal (código síncrono)
  2. Microtasks (se vacía completamente)
  3. requestAnimationFrame callbacks
  4. Una sola macrotask
  5. Repintado del navegador
  6. Repetir desde paso 2