bfcache: Come Ottimizzare il Back/Forward Cache per Navigazioni Istantanee nel 2026
Come misurare, diagnosticare e sbloccare il back/forward cache per portare le navigazioni indietro/avanti sotto i 100 ms. Con dati RUM reali, l'API NotRestoredReasons, DevTools e best practices frontend testate su siti in produzione nel 2026.
Il back/forward cache (bfcache) è una cache in memoria che il browser usa per ripristinare istantaneamente lo stato completo di una pagina quando l'utente naviga con i pulsanti indietro e avanti, riducendo la Largest Contentful Paint (LCP) e la First Contentful Paint (FCP) a valori sotto i 100 ms. Nella mia esperienza con i dati RUM di dozzine di siti, il bfcache hit rate è uno degli indicatori più sottovalutati: portarlo dal 30% al 70% sposta i p75 dei Core Web Vitals più di qualsiasi ottimizzazione sulle prime visite. Onestamente, l'ho scoperto quasi per caso su un cliente e-commerce dove avevamo esaurito le idee sulla LCP a freddo. In questa guida vediamo come misurarlo con la libreria web-vitals, come diagnosticarlo con l'API NotRestoredReasons e come sistemare il codice che lo blocca.
Il bfcache serve navigazioni indietro/avanti in meno di 100 ms senza rieseguire JavaScript, HTML parsing o network fetch.
Da Chrome 133 l'header Cache-Control: no-store non blocca più il bfcache se non ci sono cookie sensibili in gioco, ma continua a bloccarlo in Firefox e Safari.
La causa più frequente di blocco è la presenza di listener unload o beforeunload non condizionali. Sostituiscili con pagehide e visibilitychange.
L'API PerformanceNavigationTiming.notRestoredReasons ti dice esattamente perché una pagina non è stata restaurata, sia in RUM sia in DevTools.
La libreria web-vitals v4+ marca automaticamente le metriche con navigationType: 'back-forward-cache', così puoi segmentarle nei dashboard.
Un bfcache hit rate del 60% o superiore su desktop e del 40% su mobile è un obiettivo realistico per la maggior parte dei siti pubblici.
Cos'è il bfcache e perché conta per i Core Web Vitals
Il back/forward cache è una snapshot in-memory della pagina (DOM, stack JavaScript, event loop, timer sospesi) che il browser conserva quando l'utente naviga altrove. Quando l'utente preme "indietro" o "avanti", la pagina viene ripristinata anziché ricaricata: nessun round trip di rete, nessuna riesecuzione di script, nessun re-parsing del CSS. Il risultato è una navigazione che nei dati RUM di Chrome (CrUX) misura tipicamente 50–150 ms di LCP, spesso un ordine di grandezza sotto una navigazione a freddo.
Per Google il bfcache è un moltiplicatore silenzioso dei Core Web Vitals: le sessioni con navigazioni di ritorno rapide alimentano il 75° percentile che determina se il tuo sito passa la soglia "Good". Se hai già ottimizzato la LCP delle immagini con fetchpriority e AVIF, il bfcache è il complemento naturale sul lato navigazione. Vale la pena distinguerlo dalla Speculation Rules API: quella specula su navigazioni future, il bfcache restaura navigazioni passate. I due si sommano, non si escludono.
Chrome supporta il bfcache dal 2019, Firefox dal 2015 (con nome bfcache ereditato da Safari), Safari da iOS 5. La differenza sta nei criteri di eleggibilità, molto più permissivi in Chrome 133+ rispetto al 2022, quando quasi ogni Cache-Control: no-store lo azzerava.
Come misurare il bfcache hit rate con RUM
I test sintetici non ti diranno mai il tuo hit rate reale — devi misurarlo sul traffico vero. Il modo canonico è la libreria web-vitals di Google, che dalla v4 espone il tipo di navigazione in ogni metrica emessa. Ecco come inviare a un endpoint di analytics il conteggio delle restore da bfcache:
Con quel campo navigationType nel warehouse puoi calcolare l'hit rate esatto:
SELECT
device_class,
COUNTIF(navigation_type = 'back-forward-cache') AS bfcache_hits,
COUNTIF(navigation_type IN ('back-forward', 'back-forward-cache')) AS back_nav_total,
SAFE_DIVIDE(
COUNTIF(navigation_type = 'back-forward-cache'),
COUNTIF(navigation_type IN ('back-forward', 'back-forward-cache'))
) AS bfcache_hit_rate
FROM rum_events
WHERE event_date >= CURRENT_DATE() - 7
GROUP BY device_class;
Nei miei dati di campo l'hit rate desktop è quasi sempre più alto di quello mobile, perché iOS Safari è più aggressivo nell'evictare pagine sotto pressione di memoria. Segmentare per device_class e per effectiveType di rete è essenziale: un hit rate medio del 55% può nascondere un 20% catastrofico su 3G low-end.
Quali header e API bloccano il back/forward cache
Le cause di ineleggibilità si dividono in due categorie: headers HTTP e runtime API. Le principali nel 2026:
Causa
Chrome 133+
Firefox
Safari
Cache-Control: no-store
Restaurabile con restrizioni
Blocca
Blocca
Listener unload
Blocca (deprecato)
Blocca
Non blocca
Listener beforeunload
Blocca solo su desktop
Blocca
Non blocca
WebSocket aperto
Blocca
Blocca
Blocca
IndexedDB transaction pendente
Blocca
Blocca
Blocca
Cross-Origin-Opener-Policy mismatch
Blocca
Blocca
Blocca
Pagina in iframe cross-origin
Blocca (parziale)
Blocca
Blocca
La svolta di Chrome 133 sul Cache-Control: no-store merita attenzione: se non ci sono modifiche a cookie HttpOnly tra il freeze e il restore, la pagina viene servita dal bfcache anche con no-store. Questo cambia la matematica per chi ha app autenticate dove per anni si è dato per scontato che no-store equivalesse a "niente bfcache". Rimane però il vincolo di Firefox e Safari, quindi non contare solo su Chrome nei tuoi dati aggregati.
Il caso più insidioso è l'unload event: molte librerie di analytics legacy lo usano ancora per inviare l'ultimo beacon. Ogni singolo listener unload, anche vuoto, azzera l'eleggibilità. La sostituzione moderna è visibilitychange con controllo document.visibilityState === 'hidden', oppure pagehide.
NotRestoredReasons API: debug pratico
Dal 2023 Chrome espone PerformanceNavigationTiming.notRestoredReasons, standardizzato tramite la specifica MDN sul PerformanceNavigationTiming. È l'unica fonte affidabile per capire, in produzione, perché una specifica navigazione non è stata restaurata:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.type !== 'back_forward') continue;
const reasons = entry.notRestoredReasons;
if (!reasons) continue; // pagina restaurata dal bfcache
// reasons.reasons è un array di oggetti { reason: string }
navigator.sendBeacon('/rum/bfcache-blocked', JSON.stringify({
url: location.pathname,
reasons: reasons.reasons.map(r => r.reason),
blockedChildren: reasons.children?.length ?? 0,
}));
}
}).observe({ type: 'navigation', buffered: true });
I valori tipici di reason sono stringhe standardizzate come unload-handler, websocket, response-cache-control-no-store, related-active-contents. Aggregando questi dati in un dashboard scoprirai in pochi giorni quali sono i colpevoli reali sulla tua base utenti: raramente sono quelli che ti aspetti in fase di planning.
Come testare il bfcache in Chrome DevTools
Prima di andare in produzione con le modifiche, verificale in locale con il pannello dedicato. In Chrome DevTools 133+ apri Application → Background services → Back/forward cache e clicca Test back/forward cache: la pagina viene navigata via, poi restaurata, e ti mostra un report strutturato di eventuali cause di blocco divise in Actionable, Pending Support e Not Actionable. Le prime sono quelle che il tuo codice può risolvere; le seconde sono in roadmap; le terze sono limiti architetturali (es. WebUSB attivo).
Per un test più realistico, apri il tab Network, throttle a "Slow 4G", carica la pagina, naviga a un dominio esterno, torna indietro con il pulsante. Nel pannello Performance vedrai un'unica task di ripristino di pochi millisecondi anziché la solita cascata di richieste. Combinato con l'API Long Animation Frames per il debug INP, hai una visione completa del comportamento post-restore.
// Simula in DevTools il freeze/restore con questa utility
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('[bfcache] restore rilevato');
// ricarica dati stale (es. cart badge, notifiche non lette)
refreshVolatileState();
}
});
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
console.log('[bfcache] pagina messa nel bfcache');
// salva stato UI se necessario
} else {
console.log('[bfcache] pagina scaricata (no bfcache)');
}
});
Best practices per il codice frontend
Dopo aver eliminato le cause di blocco, resta un problema pratico: il tuo JavaScript continua a girare, ma è stato "congelato" per minuti o ore. Alcune parti dello stato UI vanno rinfrescate al restore. Ecco il pattern che uso in produzione:
Sostituisci unload e beforeunload con pagehide e visibilitychange. Per gli analytics beacon usa navigator.sendBeacon() dentro visibilitychange quando visibilityState === 'hidden', come consiglia anche il team di Chrome nella guida ufficiale al bfcache.
Chiudi WebSocket e SSE al pagehide e riaprili al pageshow. Non lasciarli aperti "per efficienza": bloccano il bfcache e le tue metriche di prima classe.
Rilascia gli sblocchi Web Locks e le transaction IndexedDB in modo esplicito quando la pagina passa a hidden. Le transaction pendenti sono un blocco silenzioso — non appaiono in DevTools senza notRestoredReasons.
Ricarica dati volatili sul pageshow con event.persisted === true. Cart, notifiche, timestamp relativi, banner di consenso cookie: tutti candidati.
Evita window.opener nei link esterni. Aggiungi rel="noopener" sistematicamente: oltre a essere una raccomandazione di sicurezza, evita il blocco del bfcache per related-active-contents.
Impatto reale sui Core Web Vitals nel 2026
Basta con la teoria, parliamo di numeri di campo. Nei siti che ho seguito quest'anno, un aumento del bfcache hit rate dal 25% al 65% ha spostato il p75 di LCP di 200–400 ms e il p75 di INP di 50–90 ms. La ragione è aritmetica: le navigazioni di ritorno pesano statisticamente il 15–30% del traffico su un sito editoriale medio, e per Chrome CrUX contano tanto quanto le prime visite nel calcolo del percentile che passa il threshold.
La documentazione ufficiale del Chrome UX Report conferma che dal 2024 le navigazioni bfcache-restored sono incluse nell'aggregato dei Core Web Vitals con lo stesso peso delle navigazioni fresche. Ecco perché un sito con LCP tecnicamente accettabile al p75 sui load a freddo può comunque risultare "Needs Improvement" se il bfcache non funziona: le navigazioni di ritorno gonfiano il numeratore delle sessioni "lente" quando la pagina viene ricaricata daccapo invece che restaurata.
Il mio consiglio operativo, dopo aver misurato su decine di siti: se hai già dato battaglia su LCP e INP e sei vicino alla soglia, il bfcache è probabilmente la leva rimasta col miglior rapporto sforzo/beneficio. Uno sprint di due giorni sul refactor degli unload e sulla chiusura dei WebSocket produce spesso più risultati di una settimana di image optimization. (Detto tra noi: è anche uno degli interventi con la migliore percentuale di successo al primo tentativo.)
Domande frequenti
Cos'è il bfcache in un browser?
Il bfcache (back/forward cache) è una cache in memoria che il browser mantiene delle ultime pagine visitate, con DOM, stato JavaScript e stack di esecuzione intatti. Quando l'utente clicca "indietro" o "avanti", il browser ripristina la pagina in decine di millisecondi anziché ricaricarla dalla rete.
Come verifico se la mia pagina è compatibile con il bfcache?
Apri Chrome DevTools, vai su Application → Background services → Back/forward cache e clicca "Test back/forward cache". Il report ti elenca le cause di blocco divise in Actionable, Pending Support e Not Actionable. In produzione usa l'API PerformanceNavigationTiming.notRestoredReasons per raccogliere gli stessi dati dai tuoi utenti reali.
Cache-Control: no-store impedisce ancora il bfcache?
Dipende dal browser. Da Chrome 133 il no-store non blocca più il bfcache se non ci sono cookie HttpOnly modificati durante il freeze. Firefox e Safari continuano invece a considerarlo un blocco, quindi per la copertura totale conviene rimuovere no-store dalle pagine dove non serve realmente.
Perché unload disabilita il bfcache?
Il contratto dell'evento unload impone al browser di eseguirlo prima che la pagina venga distrutta. Se la pagina è stata messa nel bfcache, non viene distrutta, quindi il browser preferisce non metterla proprio nel bfcache per rispettare la semantica. Sostituisci unload con pagehide o visibilitychange.
Il bfcache funziona con Single Page Application (SPA)?
Sì, ma solo per navigazioni tra origini diverse o verso pagine non-SPA. Le route interne di una SPA gestite via history.pushState non attraversano il bfcache perché il documento non cambia mai. Per accelerare le navigazioni interne di una SPA usa la Speculation Rules API.
La Long Animation Frames API rivela quale script blocca il main thread, con nome file, riga e funzione. Ecco come attivarla, correlarla all'INP tramite web-vitals 4.x e inviare i dati al RUM in produzione senza saturare la banda mobile.
Ottimizza i font web nel 2026 con WOFF2, font-display, preload, variable fonts, subsetting e size-adjust: ridurre CLS e LCP del 70-90% con esempi pratici.
Guida pratica a HTTP 103 Early Hints per ridurre il TTFB percepito: configurazione su Cloudflare, Vercel, Nginx e Node.js con esempi di codice e misurazione dell'impatto reale su LCP nel 2026.