Événement unload Chrome : guide de migration et Permissions-Policy

Comprenez pourquoi ‘unload’ empêche le BFCache dans Chrome et migrez vers pagehide/visibilitychange avec sendBeacon ou fetch keepalive — audit et tests pour PrestaShop 8/9.

Écrans avec du code et des diagrammes techniques pour la migration de Chrome.

Table des matières :

  1. Pourquoi l’événement unload casse le BFCache dans Chrome (et pourquoi ça vous concerne)
  2. Migration : remplacer unload par la Page Lifecycle (pagehide/visibilitychange) + envoi fiable (sendBeacon, fetch keepalive)
  3. Permissions-Policy et directive unload : quand l’utiliser, et pourquoi c’est rarement la “solution”
  4. Implémentation et tests sur PrestaShop 8/9 : audit du code, instrumentation BFCache et déploiement sans régression

Pourquoi l’événement unload casse le BFCache dans Chrome (et pourquoi ça vous concerne)

L’événement unload (et ses variantes historiques onunload, window.onunload, $(window).on('unload', ...)) a longtemps servi de « dernier crochet » pour exécuter du code au moment où l’utilisateur ferme un onglet, change d’URL ou quitte un site. Le problème, c’est que ce modèle mental ne correspond plus à la réalité des navigateurs modernes : entre les gels d’onglets, les restaurations de session, les politiques d’économie d’énergie mobiles et surtout le Back/Forward Cache (BFCache), une page n’est plus forcément “détruite” quand l’utilisateur la quitte.

MDN résume bien l’ambiguïté :

« The unload event is fired when the document or a child resource is being unloaded. »
— MDN Web Docs, “Window: unload event”
https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event

Sauf qu’“unloaded” n’est pas un état fiable pour déclencher des opérations critiques. Concrètement, sur un navigateur moderne, “quitter” une page peut vouloir dire :

  • mise en cache pour un retour instantané (BFCache) ;
  • mise en arrière-plan (onglet caché, écran verrouillé) ;
  • suspension (mobile, économie d’énergie) ;
  • vraie destruction (fermeture d’onglet, crash, memory pressure).

Côté Chrome (et Chromium au sens large), le BFCache est l’élément qui change la donne. Le BFCache conserve la page complète (DOM + heap JavaScript + état du rendu) lorsque l’utilisateur navigue “back/forward”, pour permettre un retour quasi instantané sans re-téléchargement et sans reparse complet. Dans ce mode, Chrome ne déclenche pas unload (puisque la page n’est pas détruite) et, plus important côté perf : la simple présence d’un listener unload peut rendre la page non éligible au BFCache.

Google le formule explicitement :

« Avoid using the unload event. »
— web.dev, “Back/forward cache”
https://web.dev/bfcache/

Pourquoi c’est important pour une boutique (PrestaShop ou autre) : le retour arrière fait partie du “flux naturel” d’achat. Un visiteur va typiquement :

  • revenir d’une fiche produit vers une catégorie,
  • comparer plusieurs produits en alternant “back/forward”,
  • aller-retour entre panier et fiche produit pour vérifier un détail,
  • naviguer entre pages de paiement (selon PSP) et récapitulatif.

Si BFCache est cassé, ces retours deviennent des rechargements complets : plus de CPU côté client, plus de requêtes, plus de JS à exécuter, et une impression de lenteur même si votre serveur est déjà optimisé. Sur le terrain, c’est aussi un effet “caché” : vous pouvez avoir un bon TTFB et un bon cache HTTP, tout en gardant une navigation pénible parce que chaque retour relance tout le frontend. (Si vous êtes déjà en phase de diagnostic serveur, le sujet est complémentaire à : PrestaShop lent : diagnostic TTFB, bots et optimisation serveur.)

Sur une boutique PrestaShop, unload est souvent utilisé (par le thème ou des modules) pour des besoins qui paraissent légitimes :

  • tracking “dernier instant” (flush d’événements analytics),
  • persistance de formulaire (checkout, B2B),
  • logs front/RUM envoyés en fin de page,
  • synchronisation panier côté client,
  • déclenchement d’appels tags publicitaires/attribution,
  • fermeture de connexions temps réel (chat, websockets).

Le cœur PrestaShop n’impose pas unload, mais l’écosystème le traîne encore : vieux scripts jQuery, plugins de chat, solutions d’attribution, tags injectés par un TMS, etc.

Symptômes fréquents (quand unload vous pénalise sans que ce soit évident) :

  • Retour arrière “lent” alors que la première visite est correcte.
  • “Blink” visuel : la page re-rend depuis zéro (loader, skeleton, repositionnement du scroll).
  • Panier / header qui se réinitialise (mini-panier qui “clignote”) au retour.
  • Dans Chrome DevTools, la page est indiquée comme non éligible BFCache avec une raison liée à unload / beforeunload.

Résultat : vous perdez le BFCache, vous augmentez le coût CPU côté client, et vous vous retrouvez à diagnostiquer un “site lent” côté frontend alors que vous avez déjà optimisé serveur et cache.

Migration : remplacer unload par la Page Lifecycle (pagehide/visibilitychange) + envoi fiable (sendBeacon, fetch keepalive)

La migration sérieuse ne consiste pas à “trouver un nouvel événement de sortie”, mais à revoir ce que vous essayez d’accomplir. Depuis plusieurs années, la stratégie robuste repose sur la Page Lifecycle : visibilitychange, pagehide/pageshow, et, selon les besoins et le support, freeze/resume (ces derniers sont plus “avancés” et à valider selon vos navigateurs cibles). Une référence utile côté standardisation/documentation est le dépôt WICG : https://github.com/WICG/page-lifecycle.

Le principe : réagir à une perte de visibilité ou à une transition de cycle de vie, plutôt qu’à une destruction supposée du document. Pour les retours arrière/avant, pageshow expose aussi event.persisted pour détecter une restauration BFCache.

Choisir le bon événement selon l’objectif

Dans la pratique e-commerce, on peut mapper vos anciens usages de unload vers des alternatives plus fiables :

Besoin initial (souvent vu en boutique) Mauvaise idée Alternative recommandée
Envoyer un “dernier event” analytics XHR synchrones dans unload pagehide + navigator.sendBeacon()
Sauvegarder un formulaire checkout Réseau en fin de page visibilitychange → sauvegarde locale (storage) + sync plus tard
Fermer un widget (chat) unload “cleanup” Prévoir un timeout/inactivité ou un protocole serveur tolérant aux déconnexions
Empêcher la sortie (warning) unload beforeunload si strictement nécessaire, sinon autosave + UX
Mesure perf “fin de page” unload RUM périodique + pagehide (non bloquant)

Le remplacement le plus proche fonctionnellement de unload est pagehide. Il est déclenché lorsque la page va être mise en arrière-plan ou quittée, y compris quand elle entre dans le BFCache. Là où unload “tue” la page, pagehide vous force à accepter l’idée qu’un utilisateur peut revenir avec un état intact.

// Remplacement minimaliste de unload (Chrome-friendly)
window.addEventListener('pagehide', (event) => {
  // event.persisted === true => la page part en BFCache (donc pas détruite)
  const payload = {
    url: location.href,
    ts: Date.now(),
    persisted: !!event.persisted,
  };

  // Envoi non bloquant : recommandé pour les événements de sortie
  const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
  navigator.sendBeacon('/rum/pagehide', blob);
});

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // La page revient du BFCache : réinitialisez ce qui dépend d’un état serveur volatil
    // (ex : prix dynamiques, stock, session, tokens CSRF si vous en avez côté JS)
  }
});

visibilitychange : parfait pour l’autosave (et pour éviter les “pertes”)

Pour des besoins de persistance (ex : formulaire checkout, champ de recherche, état d’un filtre), visibilitychange est souvent plus pertinent que pagehide parce qu’il se déclenche dès que le document devient hidden, pas seulement à la navigation.

Bonne pratique : ne pas dépendre d’un réseau “dernière seconde” pour ne pas perdre de données. Sauvegardez vite en local (idéalement petit payload, écritures limitées), et remontez au serveur au prochain moment opportun (intervalle, interaction suivante, ou page suivante).

Schéma simple (sans complexifier) :

  • sur visibilitychange (hidden) → sauvegarde locale (sessionStorage pour durée session, localStorage si vous assumez la persistance) ;
  • au chargement / au retour BFCache (pageshow) → restauration locale + réconciliation serveur si nécessaire.

Envoi réseau “fiable” : sendBeacon d’abord, fetch keepalive si besoin

Pour l’envoi réseau “dernier instant”, privilégiez :

  • navigator.sendBeacon() : asynchrone, non bloquant, conçu pour les scénarios de départ.
  • fetch(..., { keepalive: true }) : utile si vous avez besoin d’en-têtes spécifiques ou d’un format différent, mais à utiliser avec prudence.

Limites concrètes à connaître (à documenter pour vos devs et prestataires) :

  • keepalive a des contraintes de taille (les implémentations limitent la quantité de données “keepalive” envoyables) ; évitez les gros payloads (logs verbeux, dumps).
  • le navigateur peut abandonner l’envoi en cas de fermeture brutale / contraintes système.
  • ne comptez pas sur un “OK” applicatif : sur ces envois, votre objectif est best effort, donc votre backend doit accepter la perte partielle (et éventuellement les doublons).

Exemple minimal fetch keepalive (quand vous devez envoyer un header, par ex. un token applicatif) :

window.addEventListener('pagehide', () => {
  const body = JSON.stringify({ url: location.href, ts: Date.now() });

  fetch('/rum/exit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body,
    keepalive: true,
    credentials: 'include',
  }).catch(() => {
    // best effort : ne rien faire (ou basculer vers un retry ultérieur)
  });
});

Cas “prompt de sortie” : préférer la résilience plutôt que le blocage

Si votre ancien unload servait à afficher une confirmation utilisateur (“vous allez perdre vos modifications”), il faut être clair : ce n’est pas le bon outil.

  • C’est beforeunload qui déclenche un prompt, mais il est volontairement contraint (messages personnalisés ignorés, heuristiques anti-abus, déclenchement conditionné selon interaction utilisateur).
  • Et surtout : beforeunload est lui aussi un frein fréquent à BFCache.

Sur PrestaShop, ça concerne parfois des parcours B2B longs, des formulaires configurateurs, ou des devis. La meilleure option est de rendre la saisie résiliente (autosave + reprise) plutôt que de bloquer la navigation. Si vous êtes en refonte front, évitez de re-rentrer dans la dette jQuery/unload ; un thème moderne sans jQuery est généralement plus simple à rendre BFCache-friendly (voir : passer à Hummingbird sans jQuery).

Permissions-Policy et directive unload : quand l’utiliser, et pourquoi c’est rarement la “solution”

Permissions-Policy (ex-Feature-Policy) est un en-tête HTTP qui permet de restreindre certaines capacités du navigateur (géoloc, caméra, plein écran, etc.) par origine, et aussi au niveau des iframes. Le point important : ce n’est pas une API JavaScript, c’est un contrôle d’exécution côté navigateur.

Pour unload, il faut être prudent : ce qui règle réellement le problème BFCache, c’est la suppression des handlers unload/beforeunload et la migration vers Page Lifecycle, pas un en-tête “magique”. Dans la documentation et les articles, on voit parfois passer l’idée de “bloquer unload via une policy”, mais en pratique ce n’est pas une solution universelle, notamment parce que :

  • les directives Permissions-Policy varient selon navigateur ;
  • une directive non reconnue est généralement ignorée ;
  • et surtout, si un script tiers est injecté dans le contexte top-level (pas en iframe isolée), un header ne suffira pas à “réparer” son impact sur votre éligibilité BFCache.

L’usage le plus défendable de Permissions-Policy dans un e-commerce, c’est donc plutôt :

  • réduire la surface d’attaque et les surprises des scripts tiers,
  • expliciter ce qui est autorisé,
  • aider à durcir certaines intégrations (caméra/micro, géoloc) si vous en avez.

Et, en parallèle, traiter le sujet unload à la source : audit + suppression + remplacements.

Que faire quand un tiers “accroche” unload ?

Cas réel classique : un tag/outil marketing/attribution s’inscrit sur unload pour “finaliser” une session. Si ce script est dans votre page (pas dans une iframe sandboxée), il peut suffire à vous faire perdre BFCache.

Plan d’action pragmatique (et testable) :

  1. Identifier le script responsable (DevTools + trace addEventListener, cf. section suivante).
  2. Évaluer la valeur : l’événement “sortie” vaut-il la perte de navigation instantanée ?
  3. Changer la stratégie : demander au prestataire une implémentation compatible pagehide/sendBeacon (beaucoup l’ont déjà).
  4. Isoler si possible : quand fonctionnellement acceptable, charger certains tiers dans des iframes dédiées ou différer le chargement après consentement/interaction, pour limiter l’impact global.
  5. Documenter : si vous gardez un beforeunload, le justifier et poser une date de retrait.

Sur une boutique française/UE, vous avez aussi souvent un gestionnaire de consentement : c’est une opportunité pour réduire les scripts “always-on” qui posent problème, et ne charger certains tags qu’après consentement (et/ou après première interaction), ce qui limite mécaniquement l’exposition aux handlers non souhaités.

Configuration serveur : utile pour durcir, pas pour “réhabiliter unload”

Vous pouvez tout à fait utiliser Permissions-Policy comme élément de durcissement global (en-tête posé au reverse proxy / CDN), mais gardez l’objectif : fiabiliser et simplifier.

Exemples de configuration (à adapter à votre archi : Apache, Nginx, LiteSpeed, reverse proxy, CDN) :

# Apache (mod_headers) - exemple générique
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Nginx - exemple générique
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

L’idée ici n’est pas d’ajouter des directives “au hasard”, mais d’aligner l’en-tête sur vos usages réels (si vous n’utilisez pas la géoloc, autant la désactiver). Pour unload, retenez surtout : corriger le code reste la voie principale, et toute “policy” doit être validée en tests multi-navigateurs avant d’être considérée comme un mécanisme de prod.

Implémentation et tests sur PrestaShop 8/9 : audit du code, instrumentation BFCache et déploiement sans régression

Contexte versions (important pour éviter les conseils hors-sol) : en 2026, beaucoup de boutiques utilisent PrestaShop 8.x et certaines commencent à migrer vers 9.x. L’environnement PHP est le plus souvent PHP 8.2+ (souvent 8.2/8.3). Les problématiques unload sont principalement frontend, mais la manière de livrer la correction (bundling, cache, headers) dépend de votre stack (Apache/Nginx + PHP-FPM, CDN, reverse proxy).

Avant de toucher aux headers ou de supprimer des handlers, travaillez en staging et versionnez tout : c’est une modification qui peut impacter paiement, taggage et parcours de conversion. Si votre process de modernisation et déploiements automatisés est encore artisanal, c’est typiquement le genre de “petite modif JS” qui finit en régression prod (voir : modernisation et reprise avec déploiements automatisés).

Audit “bête et méchant” : où chercher dans un projet PrestaShop

Commencez par un grep sur le thème et les modules (y compris themes/, modules/, templates .tpl, assets compilés si vous les versionnez) :

  • onunload / beforeunload
  • addEventListener('unload' / addEventListener("unload"
  • $(window).on('unload' (legacy jQuery)
  • unload= (au cas où un attribut HTML traîne)

Sur PrestaShop, pensez aussi aux points d’injection typiques :

  • scripts ajoutés via hooks (displayHeader, displayFooter, scripts “globaux” de thème) ;
  • bundles générés (WebPack/Vite/encore selon votre setup) ;
  • tags injectés via un module type “Google Tag Manager” / “tracking” ;
  • scripts tiers ajoutés dans la configuration thème ou CMS (footer “custom JS”).

Puis validez dans Chrome DevTools : onglet Application (BFCache), onglet Performance et surtout l’outil “Back/forward cache” (ou équivalent selon version Chrome) qui explique pourquoi une page n’est pas éligible.

Instrumentation utile : prouver la cause et mesurer l’effet

Pour instrumenter côté JS sans polluer la prod, vous pouvez patcher addEventListener en dev afin de logguer les enregistrements unload (utile quand un bundle minifié masque l’origine) :

// À exécuter en console (dev uniquement)
const _add = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
  if (type === 'unload' || type === 'beforeunload') {
    console.trace('Listener', type, 'enregistré sur', this);
  }
  return _add.call(this, type, listener, options);
};

Pour mesurer côté RUM l’intérêt du BFCache (sans partir sur une usine à gaz), vous pouvez capturer deux signaux simples :

  • pageshow + event.persisted (retour depuis BFCache),
  • PerformanceNavigationTiming.type === 'back_forward' (navigation back/forward côté perf).

Exemple minimal (à envoyer sur un endpoint RUM existant) :

window.addEventListener('pageshow', (event) => {
  const nav = performance.getEntriesByType('navigation')[0];
  const payload = {
    url: location.href,
    persisted: !!event.persisted,
    navType: nav ? nav.type : undefined, // 'navigate', 'reload', 'back_forward', ...
    ts: Date.now(),
  };

  navigator.sendBeacon('/rum/pageshow', new Blob([JSON.stringify(payload)], {
    type: 'application/json'
  }));
});

Ce type de mesure est particulièrement utile quand vous déployez un correctif “simple” (suppression de unload) et que vous voulez prouver, chiffres à l’appui, que le taux de restauration BFCache remonte sur les pages clés (catégorie, produit, panier).

Migrer fonction par fonction (et gérer les doublons)

Une fois les points identifiés, migrez fonction par fonction. Exemple typique module : un script qui “flush” des événements analytics dans unload.

Remplacement minimal viable :

  • déplacer l’envoi en pagehide + sendBeacon,
  • rendre le format d’événements idempotent : côté serveur, ignorez les doublons sur un event_id (UUID, ou hash stable).
  • éviter les “gros payloads” en sortie : agréger (batch léger) plutôt que spammer 20 appels.

Pour les modules PrestaShop, évitez l’override pour ce genre de patch : corrigez au niveau du module/thème et livrez avec une stratégie propre (tests + CI, versions, rollback). Le cadre “sans override” est justement fait pour limiter la dette (voir : méthode module sans override : cadrage, tests, livraison).

Déploiement : ne pas dissocier perf navigateur et charge serveur

Quand vous améliorez BFCache et réduisez les reloads, vous changez aussi le profil de trafic :

  • moins de hits inutiles (moins de rechargements),
  • mais des sessions parfois plus longues,
  • et des parcours plus fluides qui font ressortir d’autres goulots (API, endpoints checkout, latence tags tiers).

Si vos logs montrent déjà des pics 500/503 lors de campagnes, ne déployez pas à l’aveugle : instrumentez, surveillez, et gardez un plan de rollback. Ressources complémentaires : diagnostiquer une erreur HTTP 503 (PHP/logs Apache/LiteSpeed) et stratégie cache 2026 : OPcache, Redis, Varnish et CDN.

Check de validation rapide (à exécuter avant et après)

1) Éligibilité BFCache : testez navigation back/forward sur 5 pages clés (catégorie → produit → panier → login → checkout) et notez les raisons d’inéligibilité (DevTools BFCache). Objectif : zéro blocage lié à unload / beforeunload.

2) Tracking “sortie” : comparez le volume d’événements côté backend avant/après (ex : endpoint /rum/pagehide et /rum/pageshow). Cherchez :

  • pertes sur mobile (attendues mais souvent réduites par sendBeacon),
  • doublons (à traiter via event_id côté serveur),
  • payloads trop gros (risque d’abandon).

3) Régression fonctionnelle : paiements, modules d’attribution, chat, PSP, iframes. Vérifiez en particulier les parcours où des prestataires sont chargés tard (consentement, étape checkout).

4) Cache/front : si vous compilez des assets, purge cache thème + cache navigateur (versioning), sinon vous allez conclure à tort que “ça ne marche pas”. Sur PrestaShop, les erreurs de cache applicatif et de déploiement masquent facilement un correctif JS (voir : erreurs fréquentes et bonnes pratiques cache en multiboutique).

Le point clé à retenir : sur Chrome, l’événement unload n’est plus un point d’ancrage fiable et il coûte cher en performance via l’inéligibilité BFCache. La migration durable passe par pagehide/visibilitychange + envois non bloquants (sendBeacon, fetch keepalive) et des tests BFCache systématiques en staging avant déploiement.


À lire aussi