setTimeout, setInterval y queueMicrotask en JavaScript: temporización y prioridad

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP