Event Loop en JavaScript - Cómo manejar tareas asíncronas de forma real
Lectura de 5 minutosJavaScript 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:
- Stack de ejecución: donde se ejecuta el código síncrono.
- Cola de microtasks: promesas (
Promise.then
),queueMicrotask
,MutationObserver
. - Cola de macrotasks:
setTimeout
,setInterval
, eventos del DOM. - 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
- Cada evento
input
entra como macrotask. - La llamada a
fetch
devuelve una Promise inmediatamente. - Cuando el servidor responde, la resolución de esa Promise programa sus
.then()
callbacks como microtasks. - Si el usuario escribe rápido, la macrotask siguiente cancela la petición anterior con
controller.abort()
. - 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:
- Stack principal (código síncrono)
- Microtasks (se vacía completamente)
- requestAnimationFrame callbacks
- Una sola macrotask
- Repintado del navegador
- Repetir desde paso 2