Event Loop de JavaScript
Comprende cómo funciona el event loop de JavaScript y cómo escribir código no bloqueante.
Visión General
El event loop de JavaScript es el corazón de la programación asíncrona en navegadores y Node.js. Orquesta la ejecución del código, recopila y procesa eventos, y ejecuta subtareas en cola. Comprender cómo interactúan el call stack, la cola de tareas y la cola de microtareas es esencial para escribir aplicaciones performantes y no bloqueantes.
Cuándo Usar
Usa este recurso cuando:
- Depuras errores asíncronos misteriosos o condiciones de carrera — consulta prevención de race conditions para los patrones que las causan.
- Optimizas la responsividad de la UI en aplicaciones frontend — consulta SPA code splitting para el lado del bundle en rendimiento.
- Eliges entre setTimeout, Promise y queueMicrotask para agendar trabajo.
- Entiendes por qué el orden del código no siempre coincide con el orden de ejecución.
Cuándo Evitar
- Trabajo CPU-bound: el event loop es single-threaded — mejor Web Workers.
- Procesamiento de audio/video en tiempo real: usa Web Audio API o WebRTC, que corren en threads separados.
- Paralelismo server-side: los worker threads de Node.js o child processes son mejores para tareas CPU-bound en el servidor.
Solución
Visualizando el Event Loop
console.log('1. Inicio del script');
setTimeout(() => {
console.log('2. setTimeout (macrotarea)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise (microtarea)');
});
queueMicrotask(() => {
console.log('4. queueMicrotask');
});
console.log('5. Fin del script');
// Orden de salida:
// 1. Inicio del script
// 5. Fin del script
// 3. Promise (microtarea)
// 4. queueMicrotask
// 2. setTimeout (macrotarea)
Manejando Tareas de Larga Duración
function procesarArrayGrande(arr, chunkSize = 1000) {
let index = 0;
function procesarChunk() {
const chunk = arr.slice(index, index + chunkSize);
chunk.forEach(item => computacionPesada(item));
index += chunkSize;
if (index < arr.length) {
setTimeout(procesarChunk, 0); // Ceder control al event loop
}
}
procesarChunk();
}
Async/Await y el Event Loop
async function fetchUserData(userId) {
// Await cede control al event loop
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
}
// Múltiples awaits corren secuencialmente
async function fetchSequential() {
const a = await fetch('/api/a'); // espera a
const b = await fetch('/api/b'); // espera b
// Tiempo total: time(a) + time(b)
}
// Promise.all corre concurrentemente
async function fetchParallel() {
const [a, b] = await Promise.all([
fetch('/api/a'),
fetch('/api/b'),
]);
// Tiempo total: max(time(a), time(b))
}
Web Workers para Tareas CPU-Intensivas
// main.js
const worker = new Worker('compute.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
console.log('Resultado:', e.data);
};
// compute.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
Los Web Workers corren en un thread separado. No bloquean el event loop ni la UI.
Scheduler API (Navegadores Modernos)
// Cede el main thread con scheduler.yield()
async function processChunks(items) {
for (const item of items) {
process(item);
await scheduler.yield(); // Deja que el navegador pinte
}
}
// Agenda tareas con prioridades
scheduler.postTask(() => {
// tarea visible para el usuario
}, { priority: 'user-visible' });
scheduler.postTask(() => {
// tarea en background
}, { priority: 'background' });
Node.js: setImmediate vs process.nextTick
// process.nextTick corre antes que cualquier I/O o timer
process.nextTick(() => console.log('1. nextTick'));
// setImmediate corre después de eventos I/O
setImmediate(() => console.log('3. setImmediate'));
// setTimeout corre después de un delay mínimo
setTimeout(() => console.log('2. setTimeout'), 0);
// Orden: 1. nextTick, 2. setTimeout, 3. setImmediate
// (el orden setTimeout vs setImmediate no está garantizado en la práctica)
Explicación
El event loop opera en fases:
- Call Stack: ejecuta código síncrono. Cuando está vacío, el event loop revisa las colas.
- Cola de Microtareas: procesa callbacks de Promise, queueMicrotask y MutationObserver. Se vacía completamente antes de la siguiente macrotarea.
- Cola de Macrotareas: procesa setTimeout, setInterval, setImmediate (Node.js) y eventos de I/O — una tarea por iteración del loop.
- Fase de Renderizado: los navegadores pueden actualizar el DOM y repintar si hay tiempo.
Regla crítica: todas las microtareas se ejecutan antes de la siguiente macrotarea. Esto puede bloquear la cola de macrotareas si las microtareas encolan más microtareas recursivamente.
Variantes
| Runtime | API Macrotarea | API Microtarea | Notas |
|---|---|---|---|
| Navegador | setTimeout, requestAnimationFrame | Promise, queueMicrotask | rAF corre antes del paint |
| Node.js | setTimeout, setImmediate | Promise, process.nextTick | nextTick corre antes que Promises |
| Deno | setTimeout | Promise, queueMicrotask | Se alinea con comportamiento de navegador |
| Bun | setTimeout, setImmediate | Promise, queueMicrotask | nextTick no soportado |
Avanzado: Medir Event Loop Lag
// Node.js: medir delay del event loop
let lastCheck = performance.now();
setInterval(() => {
const now = performance.now();
const lag = now - lastCheck - 1000; // intervalo esperado 1000ms
if (lag > 10) {
console.warn(`Event loop lag: ${lag.toFixed(1)}ms`);
}
lastCheck = now;
}, 1000);
// Navegador: medir long tasks con PerformanceObserver
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long task: ${entry.duration.toFixed(0)}ms`);
}
});
observer.observe({ entryTypes: ['longtask'] });
Las tareas más largas que 50ms se reportan como long tasks. Estas causan jank visible.
Avanzado: AbortController para Cancelación
const controller = new AbortController();
async function fetchWithTimeout(url, ms) {
const timer = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(timer);
}
}
Cuando se aborta, fetch rechaza con un AbortError. Esta es la forma estándar de cancelar operaciones async.
Código companion: el repo companion de javascript-event-loop tiene la demo de orden, el procesamiento por chunks y el monitor de lag como scripts ejecutables con node.
Lo que funciona
- Divide trabajo pesado en chunks: usa setTimeout o requestIdleCallback para ceder control
- Prefiere microtareas para actualizaciones DOM: queueMicrotask asegura que las lecturas DOM se agrupen
- Evita encolar microtareas recursivamente: puede congelar el event loop indefinidamente
- Usa requestAnimationFrame para actualizaciones visuales: se sincroniza con el ciclo de renderizado del navegador
- Perfila con la pestaña Performance: Chrome DevTools visualiza el timing de microtareas y macrotareas
Errores Comunes
- Asumir que setTimeout(0) es inmediato: siempre es más lento que las microtareas
- Bloquear el hilo principal: bucles síncronos >50ms causan jank y frames perdidos
- Olvidar nextTick en Node.js: process.nextTick corre antes que Promises, no después
- Recursión de microtareas: Promise.resolve().then(() => Promise.resolve().then(…)) puede bloquear
- Ignorar la fase de renderizado: colas pesadas de microtareas impiden el pintado del navegador
Solución de Problemas del Event Loop
- La UI se congela con datos grandes: un bucle síncrono está acaparando el stack. Divídelo en chunks con setTimeout o
scheduler.yield(), y vigila los long tasks >50ms en la pestaña Performance. - El timer nunca dispara cuando esperas: una cadena recursiva de microtareas está matando de hambre la cola de macrotareas. Rastrea qué encola microtareas — un
.then()que resuelve otro Promise ya resuelto loopea por siempre. - Los callbacks de setInterval se apilan: el callback tarda más que el intervalo, así que las ejecuciones se encolan sin respiro. Cambia a setTimeout recursivo para que la siguiente corrida espere a la anterior.
- Tormenta de unhandled rejections: un Promise que falla sin
.catch()loguea en cada turno del event loop y puede inundar el monitoreo. Adjunta el catch temprano, o usa handlers deunhandledrejectioncomo punto único de triaje. - La animación tartamudea pese a código “async”: las microtareas igual corren antes del paint — una cadena pesada de
awaitretrasa el renderizado. Mueve las actualizaciones visuales arequestAnimationFramey el trabajo pesado a un Worker.
Notas de Producción
- Rastrea event loop lag como métrica — una curva de lag creciente bajo carga significa que el servicio no puede procesar I/O, no que es “lento”. Alerta sobre p99 de lag en servicios Node.js.
- Presupuesta long tasks en el navegador: mantén las tareas bajo 50ms para que la fase de renderizado tenga su turno; mide con entradas
longtasken RUM. - Pon kill-switch a endpoints CPU-pesados vía timeouts u offload a workers — un bucle de parseo síncrono puede matar de hambre a todas las demás requests del proceso.
- Loguea los hot paths microtask-heavy: las cadenas recursivas de Promises que matan de hambre las macrotareas aparecen como drift de timers antes que como caídas.
Puntos Clave
- El orden es: código sync → todas las microtareas → una macrotarea → render. Todo lo demás se deduce de ahí.
awaitno bloquea — suspende y reencola la continuación como microtarea.- Si la UI hace jank, una tarea es demasiado larga; si los timers se mueren de hambre, la cola de microtareas está demasiado ocupada.
- Web Workers son la respuesta para trabajo CPU-bound; el loop en sí nunca paraleliza.
Errores Comunes en Producción
- Asumir que “código async” equivale a “no bloqueante” — las microtareas también se ejecutan antes del paint.
- Medir el lag del event loop solo en localhost; el drift de timers en producción cuenta otra historia.
- Usar setInterval para polling de alta frecuencia y dejar que las callbacks se apilen bajo carga.
- Parsear JSON pesado en el main thread;
JSON.parsesíncrono también bloquea. - Crear un Worker pero seguir haciendo el trabajo pesado en el main thread “de momento”.
- No testear la cola de microtareas recursiva — el starvation de macrotareas no aparece hasta producción.
Ver También
- MDN — Modelo de concurrencia y event loop: el recorrido canónico del modelo de runtime.
- Tasks, microtasks, queues and schedules: la referencia en profundidad sobre las peculiaridades del scheduling en navegador.
- WHATWG HTML spec — Event loops: el modelo de procesamiento normativo que implementan los navegadores.
- MDN — Prioritized Task Scheduling API: referencia de
scheduler.yield()ypostTask(). - Web performance y Core Web Vitals: cómo los long tasks se mapean a INP y métricas de velocidad de página.
Preguntas frecuentes
¿Por qué Promise.then() corre antes que setTimeout(0)?
Los callbacks de Promise entran en la cola de microtareas, que tiene mayor prioridad que la cola de macrotareas donde viven los callbacks de setTimeout. El event loop drena todas las microtareas antes de procesar la siguiente macrotarea.
¿Cuál es la diferencia entre queueMicrotask y Promise.resolve().then()?
Funcionalmente idénticos en la mayoría de casos, pero queueMicrotask es más explícito y ligeramente más eficiente. Evita crear un objeto Promise. Usa queueMicrotask cuando no necesitas la cadena Promise.
¿Cómo evito que el event loop se congele?
Divide el trabajo en chunks pequeños usando setTimeout, requestIdleCallback o Web Workers para tareas CPU-intensivas. Usa scheduler.yield() en navegadores modernos. Monitorea con PerformanceObserver para long tasks.
¿Cuál es la diferencia entre microtarea y macrotarea?
Las microtareas (callbacks de Promise, queueMicrotask, MutationObserver) corren después de que la tarea actual completa y antes de la siguiente macrotarea. Las macrotareas (setTimeout, setInterval, eventos I/O) son agendadas por el event loop. Todas las microtareas se drenan antes de que la siguiente macrotarea corra.
¿Cómo se relaciona async/await con el event loop?
await suspende la función async y cede control al event loop. Cuando el Promise awaited se resuelve, la continuación se encola como microtarea. Esto significa que las funciones async no bloquean el event loop — ceden cooperativamente.
¿Qué es requestAnimationFrame y cuándo debería usarlo?
requestAnimationFrame agenda un callback antes del siguiente paint del navegador. Corre en la fase de renderizado, después de las microtareas pero antes del paint actual. Úsalo para actualizaciones visuales: mutaciones DOM, canvas drawing, transiciones CSS. Se sincroniza con la tasa de refresco del display (usualmente 60Hz).
¿Cuál es el modelo del event loop de Node.js?
Node.js usa libuv, que tiene fases: timers, pending callbacks, idle/prepare, poll, check (setImmediate) y close callbacks. Entre cada fase, las microtareas (process.nextTick y Promises) se drenan. Esto es más estructurado que el modelo del navegador.
¿Cómo mido el rendimiento del event loop?
En Node.js, usa perf_hooks para medir event loop lag. En navegadores, usa PerformanceObserver con el tipo de entrada longtask. La pestaña Performance de Chrome DevTools visualiza el timing de tareas. Busca tareas más largas que 50ms.
¿Puede el event loop correr en paralelo?
No. JavaScript es single-threaded en el event loop. Los Web Workers y los worker threads de Node.js corren event loops separados en threads separados. SharedArrayBuffer y Atomics permiten comunicación limitada de memoria compartida.
¿Qué pasa cuando el event loop se bloquea?
La UI se congela. No hay animaciones, no hay clicks, no hay scroll. En Node.js, no se procesa I/O, no se disparan timers. El servidor se vuelve no responsivo. Por eso bloquear el main thread con trabajo síncrono es el problema de rendimiento más común en JavaScript.
Recursos Relacionados
Prevencion de Race Conditions en Codigo Async de JavaScript
Identifica y corrige race conditions en JavaScript asincrono usando secuenciacion apropiada, operaciones atomicas y patrones de Promise para ejecucion concurrente predecible
RecipeFunciones Debounce y Throttle en JavaScript
Controla la tasa de ejecución de funciones con debounce y throttle. Cubre leading y trailing edge, timers cancelables y casos de uso reales.
RecipeRendimiento SPA: Code Splitting y Lazy Loading
Mejora tiempos de carga de single-page applications dividiendo bundles a nivel de ruta y componente, implementando lazy loading con React.lazy e imports en vivo
GuideOptimización del Bundle JS: Guía Práctica Frontend
Guía práctica para medir y reducir el tamaño del bundle JavaScript con tree shaking, code splitting, reemplazo de dependencias, compresión y budgets en CI.
GuideOptimización de Performance en React
Optimiza apps de React para velocidad. Cubre memoization, virtualization, code splitting, bundle analysis, React Profiler, concurrent features y Core Web Vitals.
GuideWeb Performance y Core Web Vitals
Optimizar Core Web Vitals. Cubre LCP, INP, CLS measurement y improvement, image optimization, font loading, render-blocking resources, lazy loading, caching strategies y performance monitoring con ejemplos practicos de codigo.