TechLead

Event Loop y Concurrencia

Call stack, cola de tareas, microtareas y cómo JavaScript maneja operaciones asíncronas

Cómo JavaScript Maneja Operaciones Asíncronas

JavaScript es mono-hilo, lo que significa que solo puede ejecutar un fragmento de código a la vez. Sin embargo, maneja operaciones asíncronas como peticiones de red y temporizadores sin problemas. Esto es posible gracias al event loop, que coordina la ejecución entre el call stack y las colas de tareas.

Componentes Clave

  • Call Stack — Donde JavaScript ejecuta funciones (LIFO)
  • Web APIs — APIs proporcionadas por el navegador (setTimeout, fetch, eventos DOM)
  • Cola de Tareas (Macrotarea) — Contiene callbacks de setTimeout, setInterval, I/O
  • Cola de Microtareas — Contiene callbacks de Promesas, queueMicrotask, MutationObserver
  • Event Loop — Verifica continuamente si el stack está vacío, luego procesa las colas

El Call Stack

JavaScript usa un call stack para rastrear la ejecución de funciones:

function multiplicar(a, b) {
  return a * b;
}

function cuadrado(n) {
  return multiplicar(n, n);
}

function imprimirCuadrado(n) {
  const resultado = cuadrado(n);
  console.log(resultado);
}

imprimirCuadrado(4);

// Progresión del Call Stack:
// 1. imprimirCuadrado(4)
// 2. imprimirCuadrado(4) → cuadrado(4)
// 3. imprimirCuadrado(4) → cuadrado(4) → multiplicar(4, 4)
// 4. imprimirCuadrado(4) → cuadrado(4) ← retorna 16
// 5. imprimirCuadrado(4) ← retorna 16
// 6. Vacío (terminado)

setTimeout y la Cola de Tareas

console.log("Inicio");

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

console.log("Fin");

// Salida:
// "Inicio"
// "Fin"
// "Timeout"

// ¿Por qué? Incluso con retraso de 0ms:
// 1. console.log("Inicio") se ejecuta
// 2. Callback de setTimeout → Cola de Tareas (no call stack)
// 3. console.log("Fin") se ejecuta
// 4. Stack vacío → Event loop verifica Cola de Tareas
// 5. Callback se ejecuta → "Timeout"

Microtareas vs Macrotareas

Las microtareas tienen mayor prioridad que las macrotareas. Todas las microtareas se ejecutan antes de la siguiente macrotarea:

console.log("1. Inicio del script");

setTimeout(() => {
  console.log("4. setTimeout (macrotarea)");
}, 0);

Promise.resolve()
  .then(() => console.log("3. Promesa (microtarea)"));

console.log("2. Fin del script");

// Orden de salida:
// 1. Inicio del script
// 2. Fin del script
// 3. Promesa (microtarea)
// 4. setTimeout (macrotarea)
Microtareas (Alta Prioridad) Macrotareas (Menor Prioridad)
Promise.then/catch/finally setTimeout
queueMicrotask() setInterval
MutationObserver setImmediate (Node)
async/await (después de await) Operaciones I/O

Algoritmo del Event Loop

// Event Loop Simplificado
while (true) {
  // 1. Ejecutar todo el código síncrono en call stack
  
  // 2. Ejecutar TODAS las microtareas
  while (microtaskQueue.length > 0) {
    executeMicrotask(microtaskQueue.shift());
  }
  
  // 3. Ejecutar UNA macrotarea (si hay alguna)
  if (macrotaskQueue.length > 0) {
    executeMacrotask(macrotaskQueue.shift());
  }
  
  // 4. Renderizar actualizaciones (si es necesario, ~60fps)
  
  // 5. Repetir
}

Ejemplo Complejo

console.log("1");

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

Promise.resolve()
  .then(() => {
    console.log("3");
    setTimeout(() => console.log("4"), 0);
  })
  .then(() => console.log("5"));

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

console.log("7");

// Salida: 1, 7, 3, 6, 5, 2, 4

// Explicación:
// Síncrono: 1, 7
// Microtareas: 3, 6, 5 (cadenas de Promesas)
// Macrotarea: 2 (primer setTimeout)
// Macrotarea: 4 (setTimeout agregado durante microtarea)

async/await y el Event Loop

async function ejemploAsync() {
  console.log("1. Inicio async");
  
  await Promise.resolve();
  // Todo después de await es una microtarea
  
  console.log("3. Después de await");
}

console.log("0. Inicio del script");
ejemploAsync();
console.log("2. Fin del script");

// Salida:
// 0. Inicio del script
// 1. Inicio async
// 2. Fin del script
// 3. Después de await

// El código después de 'await' se ejecuta como microtarea

Bloqueando el Event Loop

// ❌ MALO: Código síncrono de larga duración bloquea todo
function bloquearLoop() {
  const inicio = Date.now();
  while (Date.now() - inicio < 3000) {
    // Bloquea durante 3 segundos
  }
  console.log("Terminó de bloquear");
}

// UI se congela, no se ejecutan callbacks, no hay renderizado
bloquearLoop();

// ✅ MEJOR: Dividir el trabajo
function procesarFragmento(items, indice = 0, tamanoFragmento = 100) {
  const fin = Math.min(indice + tamanoFragmento, items.length);
  
  for (let i = indice; i < fin; i++) {
    // Procesar item
  }
  
  if (fin < items.length) {
    // Ceder al event loop, luego continuar
    setTimeout(() => procesarFragmento(items, fin, tamanoFragmento), 0);
  }
}

requestAnimationFrame

// Se ejecuta antes del siguiente repintado (~60fps)
function animar() {
  // Actualizar estado de animación
  element.style.left = position + "px";
  
  // Programar siguiente frame
  requestAnimationFrame(animar);
}

requestAnimationFrame(animar);

// Prioridad: Microtareas → rAF → Macrotareas

💡 Puntos Clave

  • • JavaScript es mono-hilo pero maneja asíncronía mediante el event loop
  • • Microtareas (Promesas) se ejecutan antes que macrotareas (setTimeout)
  • • Todas las microtareas se ejecutan antes de la siguiente macrotarea
  • • Operaciones síncronas largas bloquean el event loop y congelan la UI
  • • Usa setTimeout o requestAnimationFrame para dividir trabajo pesado
  • • El código async/await después de await se ejecuta como microtarea

Práctica de orden de operaciones

  • • Escribe un script de 5 líneas mezclando Promise, setTimeout y logs síncronos; predice el orden antes de ejecutar.
  • • Inspecciona la pestaña "Timing" en Chrome Performance para ver microtareas vs macrotareas en tu código de ejemplo.
  • • Reemplaza un setTimeout(fn, 0) con queueMicrotask(fn) y observa cómo cambia el timing del renderizado.
  • • Usa requestAnimationFrame para programar actualizaciones de UI y mide el tiempo de pintado vs un timeout simple.