Back

Cómo detectar cuándo una pestaña del navegador se vuelve inactiva

Cómo detectar cuándo una pestaña del navegador se vuelve inactiva

Tienes un intervalo de polling que se dispara cada diez segundos, un vídeo reproduciéndose en segundo plano o un bucle de animación en ejecución — todos consumiendo recursos para un usuario que cambió de pestaña hace cinco minutos. La solución es sencilla una vez que conoces la API adecuada para utilizar.

Puntos clave

  • La Page Visibility API es la forma moderna y fiable de detectar cambios de visibilidad de pestañas, reemplazando los enfoques anteriores basados en window.onfocus y window.onblur.
  • Utiliza document.hidden o document.visibilityState junto con el evento visibilitychange para reaccionar cuando una pestaña se vuelve inactiva o activa.
  • Las aplicaciones prácticas incluyen pausar el polling, detener la reproducción multimedia, guardar borradores de formularios automáticamente y enviar analíticas al final de la sesión.
  • Combina visibilitychange con pagehide y navigator.sendBeacon() para un seguimiento fiable del final de sesión, y evita el evento unload porque rompe la caché de avance/retroceso.
  • Elimina siempre los listeners durante el desmontaje de componentes en aplicaciones de página única (SPA) para prevenir fugas de memoria.

La herramienta adecuada: la Page Visibility API

Las soluciones más antiguas dependían de los eventos focus y blur de windowwindow.onfocus y window.onblur. Estos detectan si la ventana del navegador tiene el foco del teclado, no si la pestaña es realmente visible. En configuraciones de pantalla dividida o flujos de trabajo con múltiples ventanas, una pestaña puede ser completamente visible sin tener el foco, y viceversa. Además, su comportamiento era inconsistente entre navegadores específicamente para el cambio de pestañas.

La Page Visibility API resuelve esto correctamente. Te indica exactamente si la página es visible para el usuario, independientemente del estado del foco.

Dos propiedades te proporcionan el estado actual:

  • document.visibilityState — devuelve "visible" o "hidden"
  • document.hidden — devuelve true cuando la pestaña está oculta, false cuando es visible

No se necesitan prefijos de proveedor en los navegadores modernos. La API tiene amplio soporte en Chrome, Firefox, Safari y Edge — tanto en escritorio como en móvil.

Escuchando el evento visibilitychange

El evento visibilitychange se dispara en document cada vez que la pestaña se oculta o vuelve a ser visible. Aquí está el patrón básico:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    console.log("Tab is hidden — pause or reduce work");
  } else {
    console.log("Tab is visible — resume work");
  }
});

También puedes leer document.visibilityState directamente si necesitas el valor de cadena en lugar de un booleano.

Casos de uso prácticos

Pausar y reanudar el polling

En lugar de realizar peticiones a la API mientras el usuario está en otra pestaña, pausa el intervalo y reanúdalo cuando regrese:

let intervalId = null;

function startPolling() {
  if (intervalId !== null) return;
  intervalId = setInterval(fetchData, 10000);
}

function stopPolling() {
  clearInterval(intervalId);
  intervalId = null;
}

document.addEventListener("visibilitychange", () => {
  document.hidden ? stopPolling() : startPolling();
});

startPolling();

Nota: los navegadores limitan (throttle) setInterval en pestañas en segundo plano en lugar de detenerlo por completo, por lo que confiar en la precisión temporal en pestañas ocultas es poco fiable de todos modos. Pausar explícitamente es más limpio. Protegerse contra intervalos duplicados también evita que se acumulen múltiples temporizadores si startPolling se llama dos veces.

Pausar la reproducción multimedia

const video = document.querySelector("video");

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    video.pause();
  } else {
    video.play().catch(() => {
      // Autoplay may be blocked by the browser; handle silently
    });
  }
});

El manejador .catch() es importante porque video.play() devuelve una promesa que puede rechazarse si el navegador bloquea la reproducción automática, lo que de otro modo generaría un error no capturado.

Guardar automáticamente un borrador de formulario

Activa un guardado cuando el usuario abandona la pestaña en lugar de en un temporizador fijo:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    saveDraft();
  }
});

Enviar analíticas antes de que el usuario abandone la página

Para datos de fin de sesión, combina el evento visibilitychange con pagehide y utiliza navigator.sendBeacon() para enviar datos de forma fiable sin bloquear la navegación:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon(
      "/analytics",
      JSON.stringify({ event: "tab_hidden" })
    );
  }
});

window.addEventListener("pagehide", () => {
  navigator.sendBeacon(
    "/analytics",
    JSON.stringify({ event: "page_unload" })
  );
});

Evita el evento unload para este propósito — no es fiable en los navegadores modernos y rompe la caché de avance/retroceso (bfcache), lo que degrada el rendimiento de la navegación. En los navegadores móviles, visibilitychange con el estado "hidden" suele ser la única señal fiable de que una sesión ha terminado, ya que pagehide y beforeunload no siempre se disparan.

Limpiando los event listeners

Elimina siempre los listeners cuando ya no sean necesarios, especialmente en aplicaciones de página única:

function handleVisibility() {
  console.log(document.visibilityState);
}

document.addEventListener("visibilitychange", handleVisibility);

// Later, when tearing down:
document.removeEventListener("visibilitychange", handleVisibility);

Recuerda que la referencia pasada a removeEventListener debe ser exactamente la misma referencia de función utilizada en addEventListener. Las funciones flecha en línea no se pueden eliminar porque cada una crea una nueva referencia.

Conclusión

La Page Visibility API — específicamente document.hidden, document.visibilityState y el evento visibilitychange — es la forma correcta y moderna de detectar cuándo una pestaña del navegador se vuelve inactiva. Funciona de manera fiable en todos los navegadores actuales, maneja el cambio de pestañas con precisión donde los eventos focus y blur de window se quedan cortos, y se combina bien con navigator.sendBeacon() y el evento pagehide para el manejo del final de sesión. Úsala para detener trabajo innecesario, guardar el estado o realizar seguimiento de la interacción, y limpia tus listeners cuando hayas terminado.

Preguntas frecuentes

Ambos informan si la página es visible, pero devuelven tipos diferentes. document.hidden devuelve un booleano, lo que es conveniente para comprobaciones condicionales rápidas. document.visibilityState devuelve una cadena, actualmente visible u hidden, que es más descriptiva y a prueba de futuro si se añaden estados adicionales más adelante. Utiliza el que mejor se adapte a tu estilo de código.

Los navegadores limitan los temporizadores en segundo plano en lugar de detenerlos, normalmente restringiéndolos a una vez por segundo o más lento. El intervalo sigue ejecutándose pero a un ritmo impredecible, por lo que la lógica sensible al tiempo se vuelve poco fiable. La solución más limpia es llamar a clearInterval dentro de tu manejador de visibilitychange y reiniciar el intervalo cuando la pestaña vuelva a ser visible.

Prefiere visibilitychange combinado con pagehide y navigator.sendBeacon. Los eventos beforeunload y unload son poco fiables en móvil y rompen la caché de avance/retroceso, perjudicando el rendimiento. El evento visibilitychange con estado hidden es la señal más consistente entre navegadores de escritorio y móviles para detectar que un usuario ha abandonado la página.

Sí, pero el comportamiento depende del padre. Un iframe generalmente hereda el estado de visibilidad de su documento padre. Ocultar el iframe en sí con CSS, como `display: none`, no dispara eventos `visibilitychange` dentro del iframe. Cada iframe tiene su propio objeto document, por lo que debes escuchar visibilitychange en el documento del iframe, no en la ventana superior.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay