Conception pour la mémoire cache avant/arrière : éligibilité, restauration et état
Apprenez comment le cache avant/arrière du navigateur restaure des pages entières, ce qui l’empêche silencieusement de fonctionner, et comment gérer l’état restauré à l’aide des fonctions pageshow et pagehide.
Sources: The Browser Feature Most Frontend Developers Completely Ignore
Lorsqu’un utilisateur appuie sur « Retour », la plupart des applications réexécutent discrètement les opérations que le navigateur aurait pu omettre : récupérer à nouveau l’HTML, exécuter tous les scripts, reconstruire le DOM et recharger les données. Le cache de navigation avant/arrière (bfcache) existe justement pour éviter cela, mais le code courant l’active souvent en silence sans que personne ne s’en aperçoive, ou gère mal la page qu’il restitue. Ce guide explique en quoi bfcache diffère du cache ordinaire, quels comportements l’empêchent de fonctionner, comment détecter et réagir à une page restaurée, ainsi que comment vérifier son fonctionnement sur ses propres écrans.
Pourquoi la navigation dans l’historique mérite une attention particulière
Les efforts de performance se concentrent généralement sur la première visite. Les équipes compressent les images, divisent les fichiers, chargent à l’avance les polices, réduisent les coûts liés au chargement initial, ajustent les appels API et surveillent le temps de rendu du contenu principal. Tout cela est utile, mais il est implicitement supposé que chaque consultation d’une page commence depuis une nouvelle fenêtre et avec une URL tapée à nouveau.
C’est pas ainsi que les gens naviguent. Un acheteur ouvre un produit, revient aux résultats, ouvre un autre article, puis retourne encore une fois. Un lecteur suit un lien depuis un article avant de revenir pour le terminer. Quelqu’un reconsulte un formulaire inachevé. Les boutons Avant et Après font partie des commandes les plus utilisées dans n’importe quel navigateur, et ces parcours sont courants plutôt que particuliers.
Les navigateurs optimisent ce schéma grâce au bfcache. Au lieu de télécharger à nouveau une page lorsque l’utilisateur s’en va, le navigateur peut conserver une capture de l’ensemble du document en mémoire. Lorsque l’utilisateur revient via l’historique de session, il peut afficher cette capture plutôt que de répéter la demande réseau, l’analyse HTML, l’exécution des scripts, le chargement des données et leur affichage. Chaque moteur majeur met en œuvre une version de cette fonctionnalité, bien que les règles d’éligibilité exactes et les mécanismes internes diffèrent d’un navigateur à l’autre.
D’un point de vue utilisateur, une restauration réussie semble presque instantanée. L’offset de défilement reste tel qu’il était, le DOM est intact, les champs de formulaire conservent souvent leurs valeurs, et la pile JavaScript continue simplement son fonctionnement. Cependant, il y a deux problèmes : de nombreuses équipes ne vérifient jamais si leurs pages remplissent les conditions, et du code qui suppose que chaque visite correspond à une page entièrement nouvelle peut se comporter de manière anormale lorsque c’est une ancienne page qui réapparaît.
Ainsi, bfcache est bien plus qu’un simple accélérateur de vitesse gratuit. Il permet de prendre en compte un cycle de vie plus réaliste : une page peut rester inactive tout en étant toujours « vivante », puis se réactiver ultérieurement sans que son code de démarrage ne s’exécute à nouveau.
En quoi bfcache diffère du cache HTTP
Le mot commun « cache » est à l’origine de la première confusion. On a tendance à penser que bfcache signifie simplement réutiliser des CSS, des scripts, des images ou des réponses API déjà téléchargées. Il s’agit là du cache HTTP, qui fonctionne à une échelle bien plus fine.
Le cache HTTP stocke des réponses individuelles. En revenant sur une page, le navigateur peut éviter certains téléchargements, mais le document lui-même doit néanmoins être reconstruit : l’HTML est analysé, les scripts sont exécutés, le framework est initialisé, le DOM est créé, les abonnements sont rétablis et de nouvelles requêtes sont envoyées.
bfcache conserve toute la page, y compris son DOM et sa pile JavaScript. Lorsque l’utilisateur quitte la page, l’exécution des scripts peut être suspendue ; lorsqu’il revient via l’historique, le navigateur affiche la page conservée et reprend son exécution. C’est pourquoi un accès au bfcache est même supérieur à un rechargement bien optimisé fourni principalement depuis le cache HTTP : il reste simplement beaucoup moins de travail à effectuer.
Cela change la manière dont vous devez aborder la navigation. Lors d’une restauration de bfcache, le parcours normal de démarrage n’est pas nécessairement exécuté à nouveau. Une variable au niveau du module peut conserver sa valeur antérieure. Le DOM reflète toujours ce que l’utilisateur a fait avant de quitter la page. Les compteurs temporels et les autres activités en temps réel reprennent là où ils s’étaient arrêtés, au lieu d’être créés à partir de zéro.
Le code conçu en partant du principe que le retour signifie toujours un rechargement complet peut donc manquer des mises à jour, conserver un état obsolète à l’écran ou créer des doublons lorsqu’il tente de configurer quelque chose qui existe déjà. Le modèle mental à adopter est simple : le navigateur n’a pas réaffiché une ancienne réponse HTML ; il a simplement réactivé une page suspendue.
Une grande avancée qui nécessite presque aucune nouvelle interface
La plupart des améliorations de performance ont un coût visible : composants redessinés, nouveaux états de chargement, rendu côté serveur ou une stratégie différente de récupération des données. bfcache est inhabituel car le navigateur met déjà en œuvre ce mécanisme.
Votre tâche n’est pas de créer un cache. Elle consiste à éviter d’empêcher le navigateur d’utiliser celui qu’il possède déjà, et à rendre le cycle de vie de votre page compatible avec sa restauration.
Cela en fait l’une des optimisations les plus efficaces à disposition de nombreuses équipes. Une page restaurée depuis la mémoire évite les transferts répétés ainsi que la majeure partie du traitement lié à une navigation normale. Les gains sont les plus importants sur des appareils et des réseaux lents, mais même les machines rapides montrent une différence claire entre la reconstruction d’une page et sa simple affichage.
Cela préserve également l’expérience utilisateur, et pas seulement le moment précis. Imaginez quelqu’un qui est à mi-chemin d’un long article, ouvre un lien pertinent puis revient en arrière. Un rechargement classique doit reconstruire la page avant de tenter de restaurer la position de défilement, souvent après que le layout ait changé. La restauration via bfcache le ramène presque exactement là où il se trouvait.
Les listes de produits illustrent ce point encore plus clairement. Quelqu’un affine un catalogue à l’aide de quatre ou cinq filtres, parcourt une longue liste, sélectionne un produit puis revient en arrière. Si la liste réapparaît immédiatement dans le même état, l’expérience semble continue. Si elle se recharge, récupère de nouvelles données, efface les filtres ou revient à la première ligne, le site paraît fragile, quelles que soient les bonnes performances observées lors du premier chargement.
Les performances ne concernent pas seulement la vitesse à laquelle les utilisateurs accèdent à une application. Elles dépendent également de l’effort répétitif que le produit leur épargne lorsqu’ils passent d’une page à l’autre.
Comment l’habitude d’utiliser unload élimine silencieusement ces avantages
L’une des raisons pour lesquelles bfcache est négligé est le fait qu’aucun problème visible ne se produit lorsque une page n’est pas éligible. Le navigateur effectue simplement une navigation habituelle dans l’historique. Tout continue de fonctionner ; les utilisateurs perdent simplement la possibilité de restaurer instantanément l’état de la page.
L’événement unload en est la cause classique. Des codes plus anciens l’utilisent pour envoyer des données d’analyse, libérer des ressources ou informer un serveur que la page se ferme, en partant du principe que la navigation détruit toujours la page et offre ainsi une dernière chance de nettoyer les ressources associées.
Cette théorie ne correspond plus à la réalité. Une page peut être suspendue en raison de bfcache au lieu d’être supprimée, et unload est depuis longtemps peu fiable, en particulier sur les appareils mobiles où il ne s’exécute souvent jamais. Le comportement varie d’un navigateur à l’autre, mais enregistrer un écouteur unload peut rendre une page inéligible à bfcache dans certains d’entre eux. Les directives actuelles de la plateforme déconseillent fortement l’utilisation de unload et recommandent des événements de cycle de vie adaptés au modèle moderne. Au moment où ce texte est écrit, Chrome est également en train de déprécier unload ; veuillez donc consulter la documentation actuelle du navigateur pour connaître son statut le plus récent.
Le gestionnaire d’événement n’a même pas besoin de provenir de votre propre code. Des extraits d’analyse, des balises publicitaires, des widgets de chat pour le support technique ainsi que des dépendances plus anciennes enregistrent parfois un tel gestionnaire en interne. Il passe l’examen du code car il n’a aucun effet visible, mais il peut supprimer une optimisation au niveau du navigateur à chaque visite de cette page.
Remplacer unload ne consiste pas simplement à renommer l’événement. Le bon substitut dépend de la véritable fonction du code :
- L’analyse peut être envoyée à l’aide de
navigator.sendBeacon()ou d’une requêtefetchcorrectement configurée (par exemple aveckeepalive) lorsque la visibilité de la page passe en mode caché. - Les avertissements concernant des données de formulaire non enregistrées peuvent utiliser
beforeunload, mais ce gestionnaire ne doit être ajouté que tant qu’il y a des modifications non enregistrées et supprimé une fois celles-ci sauvegardées, plutôt que d’être attaché pendant toute la durée de vie de la page.
Leçon plus générale : quitter une page ne signifie pas la fin de son cycle de vie. Le code basé sur cette logique devient à la fois moins fiable et plus lent qu’il ne le devrait.
Détecter la restauration avec pageshow et pagehide
Deux événements encadrent cette période cachée. pagehide est déclenché lorsque la navigation fait disparaître le document de l’écran, et pageshow est déclenché lorsque la navigation rend à nouveau un document actif, y compris lors d’une restauration depuis bfcache. Ces deux événements possèdent une propriété persisted. Pour pageshow, persisted vaut true lorsque la page est sortie de bfcache ; pour pagehide, il indique que le navigateur tente de conserver la page. Contrairement à unload, pagehide a été conçu pour coexister avec bfcache.
Le fragment ci-dessous montre la forme de base. Le gestionnaire pageshow renvoie précocement lors d’un chargement normal, de sorte que la logique de mise à jour ne s’exécute qu’en cas de retour d’une page conservée. Le gestionnaire pagehide suspend les connexions en temps réel chaque fois que la page est cachée, qu’elle soit stockée en cache ou non. Les fonctions d’aide servent de placeholders pour votre propre logique d’application.
window.addEventListener("pageshow", (event) => {
if (!event.persisted) {
return;
}
refreshSessionState();
refreshTimeSensitiveData();
});
window.addEventListener("pagehide", () => {
pauseRealtimeConnections();
});
Remarquez ce que cela ne fait pas : il ne recharge pas tout. Exécuter à nouveau toute la couche de données à chaque restauration annulerait une grande partie des avantages. L’objectif est de mettre à jour uniquement les informations dont la justesse peut changer de manière significative pendant que la page est en suspension.
Cela signifie différentes choses selon la page :
- Une page d’accueil de nouvelles pourrait vouloir afficher de nouveaux titres.
- Une page d’accueil de compte pourrait avoir besoin de vérifier que la session n’a pas été terminée dans une autre fenêtre.
La véritable amélioration réside dans le fait que l’application peut désormais distinguer un chargement frais d’une restauration. Sans cette distinction, les équipes doivent généralement choisir entre deux options médiocres : ne pas actualiser et risquer des contenus obsolètes, ou réexécuter toute l’initialisation et perdre une grande partie de la vitesse. Une application qui comprend bfcache traite la restauration comme une phase distincte du cycle de vie, en conservant ce qui est encore valide et en mettant à jour ce dont on ne peut plus faire confiance.
Lorsque l’état restauré devient un problème de correction
La restauration instantanée semble purement positive jusqu’à ce que la page conservée contienne des éléments qui ne devraient plus être affichés.
Imaginons un utilisateur qui ouvre le tableau de bord de son compte, passe à une autre page, se déconnecte d’une deuxième fenêtre et appuie finalement sur Retour. L’aperçu restauré peut encore afficher des détails de la session précédente. Le navigateur ne viole aucune règle de sécurité en mémorisant une page au sein de la même session de navigation, mais votre application peut devoir vérifier que la session est toujours valide avant d’autoriser toute interaction supplémentaire.
Les données sensibles au temps posent le même problème. Un billet a pu être acheté dans une autre fenêtre, un enregistrement modifié par un collègue, ou une période d’disponibilité peut avoir pris fin. Une page restaurée peut être une représentation parfaitement fidèle du passé, mais rester la mauvaise source de vérité pour le présent.
pageshow est l’endroit idéal pour effectuer cette mise à jour. Les directives officielles indiquent clairement que les pages dont le contenu change fréquemment peuvent récupérer de nouvelles données une fois restaurées, plutôt que d’opter complètement pour l’exclusion du bfcache.
Cache-Control: no-store évolue constamment. Chrome, par exemple, a commencé à autoriser certaines pages marquées no-store à être stockées dans le bfcache lorsque des conditions spécifiques sont remplies. Il convient d’éviter de se baser sur une hypothèse simpliste concernant la façon dont chaque navigateur gère cet en-tête, et de consulter la documentation actuelle du navigateur avant de s’y fier.
La méthode la plus sûre consiste à décider explicitement :
- quel état peut être conservé en l’état sans risque ;
- quel état doit être révalidé après une restauration ;
- quelles actions de l’utilisateur exigent toujours une vérification d’autorisation fraîche sur le serveur, quel que soit l’affichage actuel de l’interface.
Cette dernière catégorie est la plus importante. Le client peut mettre à jour son affichage, mais le serveur ne doit jamais se fier à une page restaurée pour prouver qu’un utilisateur est toujours connecté.
Pause des connexions à long terme pendant la suspension
Les connexions ouvertes telles que les WebSockets, les handles IndexedDB, les verrous partagés et les requêtes en cours compliquent le fonctionnement de bfcache, car le navigateur doit geler la page tandis que d’autres onglets et documents continuent de fonctionner. Une page gelée qui détient un verrou partagé ou une connexion à une base de données ouverte peut entraver le fonctionnement des pages encore actives. Les règles d’éligibilité varient selon les navigateurs, et dans certains cas, une connexion ouverte ou active suffit pour empêcher une page d’être stockée dans le cache. La méthode recommandée consiste à libérer ou suspendre de tels ressources lorsque la page est cachée, puis à les rétablir avec prudence une fois qu’elle devient visible.
Cela ne signifie pas pour autant qu’il faille supprimer tous les écouteurs d’événements ou observateurs. Ceux qui se trouvent entièrement à l’intérieur de la page, tels que les écouteurs DOM, peuvent généralement rester actifs sans aucun problème. Il convient plutôt de se concentrer sur tout ce qui s’étend au-delà du document figé vers des systèmes externes, ou sur tout ce qui ne devrait pas paraître actif lorsque personne ne consulte la page.
Un tableau de bord en temps réel, par exemple, pourrait fermer son WebSocket lors de l’événement pagehide et en ouvrir un nouveau lorsque la page est restaurée. L’implémentation doit éviter les doublons, car tant le processus normal de démarrage que le gestionnaire de restauration peuvent tenter de se connecter ; une seule fonction connect() qui vérifie s’une connexion existe déjà constitue un moyen simple de rendre ce processus idempotent. Un wrapper IndexedDB pourrait fermer sa connexion à la base de données avant suspension et la rouvrir uniquement lorsque c’est nécessaire.
Cette discipline apporte des avantages au-delà de bfcache. Elle oblige l’équipe à déterminer qui est responsable de chaque ressource externe, quand elle doit exister et ce qui se passe lorsque le document devient inactif. Ces contrats sont tout aussi utiles pour les onglets en arrière-plan, les appareils mobiles qui figent les pages et les sessions interrompues.
Les applications single-page continuent de bénéficier
bfcache fonctionne sur les navigations entre documents gérés par le navigateur. Les changements de route gérés en JavaScript, où aucun nouveau document n’est chargé, restent en dehors de son champ d’application. Passer de /products à /products/42 via un routeur côté client relève toujours de la gestion propre de l’SPA et de son framework.
Même ainsi, les SPAs ont beaucoup à gagner. Les utilisateurs quittent fréquemment une application pour se rendre sur une page distincte, un site tiers, un fournisseur d’authentification ou une autre application complètement différente, avant de revenir plus tard. Dans ce cas, le navigateur peut récupérer l’application dans son intégralité au lieu de télécharger à nouveau les fichiers, de rérender chaque composant et de relancer les étapes d’initialisation. Les directives officielles soulignent explicitement cet avantage, tout en indiquant que les changements de route au sein de l’application échappent au contrôle de bfcache.
Ces retours peuvent révéler des hypothèses au niveau du framework :
- Une initialisation racine prévue pour s’exécuter chaque fois que le document devient visible ;
- Une couche de données qui suppose que ses caches commencent toujours vides ;
- Une connexion en temps réel recréée alors que l’ancienne est également reprise ;
- Un composant affichant des données qui étaient correctes au moment du départ mais qui sont désormais potentiellement obsolètes.
Aucun framework ne masque le cycle de vie propre du navigateur. Que vous utilisiez React, Vue, Angular ou Svelte, le code s’exécute toujours à l’intérieur d’un document que le navigateur peut conserver et restaurer. Un framework est efficace pour organiser l’état, mais il ne peut pas déterminer à votre place quels données métier restent valides après une suspension. Dans React, par exemple, les composants ne se rémontent pas lors du restauration de bfcache, donc les effets exécutés au moment du montage ne se réexécuteront pas ; c’est grâce à un écouteur pageshow que l’application perçoit que du temps s’est écoulé.
De nombreux développeurs front-end connaissent bien mieux le comportement de montage et de démontage de leur framework que le cycle de vie du document du navigateur. bfcache montre que le montage et le démontage ne décrivent qu’un seul aspect de ce qui se passe réellement sur une page.
Tester bfcache en quelques minutes
bfcache reste ignoré en partie parce qu’il est difficile de déterminer s’il fonctionne. Le retour à la page peut déjà sembler rapide, et le panneau Réseau n’est pas très utile, car une page restaurée saute complètement la séquence normale de requêtes.
Chrome DevTools dispose d’une vérification spécialement conçue pour bfcache, accessible dans le panneau Application. Lorsque vous l’exécutez, Chrome navigue vers et depuis la page, puis indique si celle-ci a été restaurée. Lorsque la page ne remplit pas les conditions, DevTools liste des raisons concrètes et identifie quels frames en sont responsables.
Faites de cette vérification une partie des contrôles réguliers de performance pour les pages importantes, en particulier les résultats de recherche, les articles, les listes de produits, les tableaux de bord et tout écran que les utilisateurs consultent fréquemment. Exécutez-la également avec des scripts tiers activés, car la conformité est souvent bloquée par du code provenant de sources externes à votre propre bundle.
Pour les données de terrain, l’information notRestoredReasons fournie via l’API Performance (dans la rubrique relative au temps de navigation) peut expliquer pourquoi les navigations historiques réelles n’ont pas été restaurées. Ce support n’est pas universel, il convient donc de le considérer comme un outil d’instrumentation progressive plutôt que quelque chose sur quoi votre code peut compter dans tous les navigateurs. Il reste néanmoins précieux, car aucun test local ne peut reproduire toutes les extensions de navigateur, les iframe tiers, les connexions ouvertes, les en-têtes ou les conditions de navigateur que vos utilisateurs apportent avec eux. Une alternative légère consiste à enregistrer la valeur de event.persisted provenant d’événements pageshow dans vos outils d’analyse afin de mesurer votre taux réel de visites.
Ne vous attendez pas à ce que chaque navigation dans l’historique accède au cache. Les navigateurs éliminent les snapshots pour gérer la mémoire, et certains obstacles échappent au contrôle de la page. L’objectif réaliste est d’éliminer les obstacles évitables et de gérer correctement la restauration chaque fois que cela se produit.
Conclusion
bfcache est facile à négliger précisément parce que rien dans votre code ne l’active. Il n’existe ni package, ni composant, ni fichier de configuration qui le fasse apparaître comme faisant partie de l’architecture ; le navigateur cherche simplement à offrir aux utilisateurs une meilleure expérience. Cette invisibilité a des effets doubles : lorsqu’il fonctionne, les utilisateurs bénéficient d’une navigation dans l’historique quasi instantanée avec presque aucun code requis ; lorsqu’il est bloqué, le site ralentit sans qu’aucune erreur ne soit affichée ; et lorsque la restauration est mal gérée, un état obsolète réapparaît car l’application suppose que revenir en arrière signifie toujours recharger la page.
Une liste de contrôle pratique pour vos pages les plus fréquemment consultées :
- Supprimez les écouteurs
unload, y compris ceux ajoutés par des scripts tiers, et transférez leurs fonctions àpagehide,visibilitychangeousendBeacon(). - Attachez
beforeunloaduniquement lorsqu’il y a des modifications non enregistrées. - Fermez ou mettez en pause les connexions externes dans
pagehide, puis réconnectez-les de manière idempotente lors du redémarrage de la page. - Utilisez
pageshowavecevent.persistedpour actualiser uniquement les données susceptibles de devenir obsolètes, et conservez les vérifications d’autorisation sur le serveur. - Exécutez le test de cache avant/arrière des outils de développement et suivez les taux de restauration dans des conditions réelles lorsque les API le permettent.
Le changement fondamental réside dans la manière dont on conçoit une page. Plutôt que d’envisager une ligne droite allant de la création à la destruction, son cycle de vie peut inclure des étapes telles que la configuration, le masquage, la suspension, la restauration, la réconciliation et la poursuite du fonctionnement. Les équipes consacrent beaucoup d’efforts à accélérer la reconstruction des pages, tandis que le navigateur est souvent prêt à omettre complètement cette reconstruction et à restituer aux utilisateurs la page qu’ils possédaient déjà.