Comment détecter qu'un onglet de navigateur devient inactif
Vous avez un intervalle de polling qui se déclenche toutes les dix secondes, une vidéo qui joue en arrière-plan ou une boucle d’animation en cours — autant de ressources consommées pour un utilisateur qui a changé d’onglet il y a cinq minutes. La solution est simple, à condition de connaître la bonne API à utiliser.
Points clés à retenir
- L’API Page Visibility est la méthode moderne et fiable pour détecter les changements de visibilité d’un onglet, remplaçant les anciennes approches
window.onfocusetwindow.onblur. - Utilisez
document.hiddenoudocument.visibilityStateconjointement avec l’événementvisibilitychangepour réagir lorsqu’un onglet devient inactif ou actif. - Les applications pratiques incluent la mise en pause du polling, l’arrêt de la lecture multimédia, la sauvegarde automatique de brouillons de formulaires et l’envoi de données analytiques de fin de session.
- Associez
visibilitychangeàpagehideetnavigator.sendBeacon()pour un suivi fiable de fin de session, et évitez l’événementunloadqui casse le cache back/forward. - Pensez toujours à retirer les écouteurs lors du démontage des composants dans les applications monopages afin de prévenir les fuites mémoire.
L’outil approprié : l’API Page Visibility
Les anciennes solutions reposaient sur les événements focus et blur de window — window.onfocus et window.onblur. Ceux-ci détectent si la fenêtre du navigateur possède le focus clavier, et non si l’onglet est réellement visible. Dans les configurations en écran partagé ou les workflows multi-fenêtres, un onglet peut être entièrement visible sans avoir le focus, et inversement. Leur comportement était également incohérent d’un navigateur à l’autre, en particulier pour le changement d’onglet.
L’API Page Visibility résout ce problème correctement. Elle vous indique précisément si la page est visible pour l’utilisateur, indépendamment de l’état du focus.
Deux propriétés vous renseignent sur l’état actuel :
document.visibilityState— renvoie"visible"ou"hidden"document.hidden— renvoietruelorsque l’onglet est masqué,falselorsqu’il est visible
Aucun préfixe propriétaire n’est nécessaire dans les navigateurs modernes. L’API bénéficie d’une large prise en charge sur Chrome, Firefox, Safari et Edge — aussi bien sur ordinateur que sur mobile.
Écouter l’événement visibilitychange
L’événement visibilitychange se déclenche sur document à chaque fois que l’onglet devient masqué ou redevient visible. Voici le schéma de base :
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
console.log("Tab is hidden — pause or reduce work");
} else {
console.log("Tab is visible — resume work");
}
});
Vous pouvez également lire directement document.visibilityState si vous préférez la valeur sous forme de chaîne plutôt qu’un booléen.
Cas d’usage pratiques
Mettre en pause et reprendre le polling
Plutôt que de continuer à envoyer des requêtes API lorsque l’utilisateur est sur un autre onglet, mettez l’intervalle en pause et reprenez-le à son retour :
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();
À noter : les navigateurs limitent (throttling) setInterval dans les onglets en arrière-plan plutôt que de l’arrêter complètement, donc se fier à la précision temporelle dans des onglets masqués n’est de toute façon pas fiable. Une mise en pause explicite est plus propre. La protection contre les intervalles dupliqués évite également l’empilement de plusieurs timers si startPolling est appelée deux fois.
Mettre en pause la lecture multimédia
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
});
}
});
Le gestionnaire .catch() est important car video.play() retourne une promesse qui peut être rejetée si le navigateur bloque la lecture automatique, ce qui provoquerait sinon une erreur non interceptée.
Sauvegarder automatiquement un brouillon de formulaire
Déclenchez une sauvegarde lorsque l’utilisateur quitte l’onglet plutôt que sur un minuteur fixe :
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
saveDraft();
}
});
Discover how at OpenReplay.com.
Envoyer des données analytiques avant le départ de l’utilisateur
Pour les données de fin de session, combinez l’événement visibilitychange avec pagehide et utilisez navigator.sendBeacon() pour envoyer les données de manière fiable sans bloquer la navigation :
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" })
);
});
Évitez l’événement unload pour cet usage — il est peu fiable dans les navigateurs modernes et casse le cache back/forward (bfcache), ce qui dégrade les performances de navigation. Sur les navigateurs mobiles, visibilitychange avec l’état "hidden" est souvent le seul signal fiable indiquant qu’une session est terminée, puisque pagehide et beforeunload ne se déclenchent pas systématiquement.
Nettoyer les écouteurs d’événements
Retirez toujours les écouteurs lorsqu’ils ne sont plus nécessaires, en particulier dans les applications monopages :
function handleVisibility() {
console.log(document.visibilityState);
}
document.addEventListener("visibilitychange", handleVisibility);
// Later, when tearing down:
document.removeEventListener("visibilitychange", handleVisibility);
Souvenez-vous que la référence passée à removeEventListener doit être exactement la même référence de fonction que celle utilisée dans addEventListener. Les fonctions fléchées en ligne ne peuvent pas être retirées, car chacune crée une nouvelle référence.
Conclusion
L’API Page Visibility — plus précisément document.hidden, document.visibilityState et l’événement visibilitychange — constitue la méthode moderne et correcte pour détecter qu’un onglet de navigateur devient inactif. Elle fonctionne de manière fiable sur tous les navigateurs actuels, gère précisément le changement d’onglet là où les événements focus et blur de window sont insuffisants, et s’intègre bien avec navigator.sendBeacon() et l’événement pagehide pour la gestion de fin de session. Utilisez-la pour interrompre les traitements inutiles, sauvegarder l’état ou suivre l’engagement, et n’oubliez pas de nettoyer vos écouteurs une fois terminé.
FAQ
Les deux indiquent si la page est visible, mais ils renvoient des types différents. document.hidden renvoie un booléen, ce qui est pratique pour des vérifications conditionnelles rapides. document.visibilityState renvoie une chaîne, actuellement soit visible soit hidden, plus descriptive et pérenne si d'autres états sont ajoutés ultérieurement. Utilisez celui qui correspond le mieux à votre style de code.
Les navigateurs limitent (throttling) les minuteurs en arrière-plan plutôt que de les arrêter, en général en les bornant à une exécution par seconde ou moins. L'intervalle continue de tourner, mais à des cadences imprévisibles, ce qui rend la logique sensible au temps peu fiable. La solution la plus propre consiste à appeler clearInterval à l'intérieur de votre gestionnaire visibilitychange et à redémarrer l'intervalle lorsque l'onglet redevient visible.
Privilégiez visibilitychange combiné à pagehide et navigator.sendBeacon. Les événements beforeunload et unload sont peu fiables sur mobile et cassent le cache back/forward, ce qui nuit aux performances. L'événement visibilitychange avec l'état hidden est le signal le plus cohérent entre les navigateurs desktop et mobiles pour détecter qu'un utilisateur a quitté la page.
Oui, mais le comportement dépend du parent. Une iframe hérite généralement de l'état de visibilité de son document parent. Masquer l'iframe elle-même via CSS, par exemple avec `display: none`, ne déclenche pas d'événements `visibilitychange` à l'intérieur de l'iframe. Chaque iframe possède son propre objet document, vous devez donc écouter visibilitychange sur le document de l'iframe, et non sur la fenêtre du haut.
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.