setTimeout, setInterval y las funciones relacionadas controlan cuándo se ejecuta el código asíncrono en JavaScript. Aunque su uso básico es sencillo, tienen comportamientos sutiles relacionados con el event loop que conviene conocer para evitar el drift de intervalos, el tiempo mínimo real de espera y las diferencias de prioridad entre distintas colas de ejecución.
setTimeout: ejecutar una vez tras un retraso
setTimeout(fn, delay) programa la ejecución de fn al menos delay milisegundos después. El delay no es preciso: si el call stack está ocupado, el callback se retrasa:
// Básico
const id = setTimeout(() => {
console.log('Ejecutado después de 1 segundo');
}, 1000);
// Cancelar antes de que se ejecute
clearTimeout(id);
// setTimeout(fn, 0) no es inmediato:
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Orden: 1, 3, 2 (el callback va a la cola de macrotasks)
// Pasar argumentos al callback:
setTimeout((nombre, accion) => {
console.log(`${nombre}: ${accion}`);
}, 500, 'Sistema', 'iniciado');
// 'Sistema: iniciado'
setInterval: ejecutar repetidamente
setInterval(fn, delay) programa fn para ejecutarse cada delay milisegundos. El problema: si la ejecución del callback tarda más que el intervalo, las llamadas se acumulan o se solapan dependiendo del entorno:
let contador = 0;
const intervalo = setInterval(() => {
contador++;
console.log(`Tick ${contador}`);
if (contador >= 5) clearInterval(intervalo);
}, 1000);
// El problema del drift con setInterval:
// Si el callback tarda 200ms y el intervalo es 1000ms,
// en realidad se ejecuta cada 1200ms (drift acumulado)
// Alternativa sin drift: setTimeout recursivo
function intervaloSinDrift(fn, delay) {
let activo = true;
function tick() {
if (!activo) return;
const inicio = Date.now();
fn();
const tardado = Date.now() - inicio;
// Ajustar el delay para compensar el tiempo del callback
setTimeout(tick, Math.max(0, delay - tardado));
}
setTimeout(tick, delay);
return () => { activo = false; };
}
const detener = intervaloSinDrift(() => {
console.log('Tick puntual:', new Date().toLocaleTimeString());
}, 1000);
// Detener después de 5 segundos:
setTimeout(detener, 5000);
queueMicrotask: ejecutar en la cola de microtasks
queueMicrotask(fn) pone fn en la cola de microtasks, que tiene prioridad sobre los macrotasks de setTimeout. Equivale a Promise.resolve().then(fn) pero es más eficiente:
console.log('1 - síncrono');
setTimeout(() => console.log('4 - macrotask'), 0);
queueMicrotask(() => console.log('3 - microtask'));
Promise.resolve().then(() => console.log('3 - promise .then'));
console.log('2 - síncrono');
// Orden: 1, 2, 3 (microtask y promise), 3 (promise), 4 (macrotask)
// Las microtasks siempre se ejecutan antes de la siguiente macrotask
// Cuándo usar queueMicrotask:
// Diferir trabajo sin ceder antes de que termine el código actual,
// pero con más prioridad que setTimeout
function notificarCambio(callback) {
queueMicrotask(() => {
// Se ejecuta tras el código síncrono actual, antes del siguiente setTimeout
callback();
});
}
requestAnimationFrame: sincronizado con el refresco de pantalla
requestAnimationFrame(fn) programa fn antes del próximo repintado del navegador (típicamente ~16ms a 60fps). Es la forma correcta de animar en el navegador:
// Animación con requestAnimationFrame
function animar(elemento) {
let posicion = 0;
let frameId;
function frame(timestamp) {
posicion += 2;
elemento.style.transform = `translateX(${posicion}px)`;
if (posicion < 300) {
frameId = requestAnimationFrame(frame);
}
}
frameId = requestAnimationFrame(frame);
return () => cancelAnimationFrame(frameId);
}
// Por qué rAF es mejor que setInterval para animaciones:
// - Se pausa cuando la pestaña no está visible (ahorra batería)
// - Sincronizado con el refresco real del monitor
// - No se acumula el drift
// Throttle de scroll con rAF:
let ultimoFrame = null;
window.addEventListener('scroll', () => {
if (ultimoFrame) return;
ultimoFrame = requestAnimationFrame(() => {
console.log('ScrollY:', window.scrollY);
ultimoFrame = null;
});
});
El tiempo mínimo real de setTimeout
Los navegadores aplican un delay mínimo de 4ms para llamadas anidadas de setTimeout o cuando la pestaña está en segundo plano:
// En la práctica, setTimeout(fn, 0) puede tardar 4ms o más // En pestañas en segundo plano: hasta 1000ms (throttling del navegador) // Para código que necesita ejecutarse tan pronto como sea posible // después del stack actual, usa Promise o queueMicrotask: // Más rápido que setTimeout(fn, 0): Promise.resolve().then(fn); queueMicrotask(fn); // Equivalentes aproximados en prioridad: // síncrono > microtasks > requestAnimationFrame > macrotasks (setTimeout/setInterval)
La distinción práctica: usa setTimeout para retrasos reales o para diferir trabajo de baja prioridad, queueMicrotask cuando necesitas ejecutar algo después del stack pero antes de la siguiente macrotask, setInterval solo para tareas simples con baja frecuencia, y requestAnimationFrame siempre que trabajes con animaciones o actualizaciones visuales.
