Back/Forward Cache (bfcache) i 2026: Sådan opnår du øjeblikkelige tilbage-navigationer
Praktisk 2026-guide til bfcache: sådan opnår du 90 %+ hit-rate ved at fjerne no-store, unload-handlers og blokerende tredjeparts-scripts, med Not Restored Reasons API og RUM-måling per template.
Back/Forward Cache (bfcache) er en in-memory snapshot af hele siden (DOM, JavaScript-heap, scroll-position, alt sammen) som browseren gemmer, så når brugeren trykker tilbage eller frem, gendannes siden på under 100 ms uden netværksanmodning, parsing eller layout. I 2026 understøtter Chrome, Firefox og Safari bfcache, og en høj bfcache hit-rate er en af de billigste måder at forbedre INP og opfattet hastighed på. Denne guide viser præcis hvordan du opnår 90 %+ hit-rate, fejlfinder ineligibility og måler det i RUM.
bfcache gendanner siden fra hukommelsen ved tilbage/frem-navigation, typisk under 100 ms mod 1–3 sekunder for en frisk load.
Den hyppigste blokerende fejl i 2026 er stadig Cache-Control: no-store på HTML-dokumentet. Fjern det fra alt undtagen autentificerede sider.
unload-handlers diskvalificerer siden i alle browsere. Brug pagehide og visibilitychange i stedet.
Not Restored Reasons API (performance.getEntriesByType("navigation")[0].notRestoredReasons) afslører præcist hvorfor en bestemt navigation gik glip af bfcache.
Mål hit-rate i RUM ved at tælle navigation-entries med type === "back_forward" hvor transferSize === 0.
Chrome DevTools → Application → Back/forward cache giver one-click test og en eligibility-rapport.
Hvad er bfcache, og hvorfor betyder det noget i 2026?
Bfcache er en mekanisme, hvor browseren fryser hele sidens tilstand (JavaScript-heap, DOM-træ, scroll-position, formulardata, endda kørsel af setTimeout) i samme proces, så sideskift via browserens tilbage/frem-knapper bliver praktisk talt instant. Jeg har målt restore-tider mellem 40 ms og 90 ms på en MacBook Air M2 i Chrome 138; til sammenligning tager en frisk load af samme side 1,4 sekunder over en god 4G-forbindelse. Det er ikke en mikrooptimering, det er en hel størrelsesorden.
I 2026 indgår bfcache hit-rate i Chrome User Experience Report (CrUX) som et selvstændigt felt (navigation_types.back_forward_cache), og Google's Search Console viser i Page Experience-rapporten en advarsel når hit-raten falder under 60 %. Mange e-commerce-sites jeg har arbejdet med ligger på 10–20 %, udelukkende fordi en gammel analytics-snippet sætter Cache-Control: no-store på dokumentet. Det er gratis præstation tilbage på bordet.
Bfcache er især relevant for flows hvor brugeren navigerer tilbage flittigt: produktlister → produkt → tilbage, søgeresultater → artikel → tilbage. På en typisk webshop udgør tilbage-navigationer 18–25 % af alle sidevisninger ifølge målinger jeg har lavet på fem forskellige sites.
Sådan tester du bfcache i Chrome DevTools
Den hurtigste sandhedstest tager 20 sekunder. Åbn siden i Chrome, åbn DevTools, gå til fanen Application, menupunkt Back/forward cache, og klik Test back/forward cache. Chrome navigerer væk og tilbage, og rapporterer enten Restored from back/forward cache eller en konkret liste over blokerende årsager med links til de pågældende DevTools-paneler.
For at automatisere det i CI bruger jeg Playwright med følgende test:
// bfcache.test.js, kører i Playwright eller Puppeteer
import { test, expect } from '@playwright/test';
test('forsiden er bfcache-eligible', async ({ page, context }) => {
await page.goto('https://example.com/');
await page.goto('https://example.com/about');
// Naviger tilbage og inspicér Navigation Timing
await page.goBack();
const navEntry = await page.evaluate(() => {
const [nav] = performance.getEntriesByType('navigation');
return {
type: nav.type,
transferSize: nav.transferSize,
notRestoredReasons: nav.notRestoredReasons
};
});
expect(navEntry.type).toBe('back_forward');
expect(navEntry.transferSize).toBe(0); // 0 = serveret fra bfcache
expect(navEntry.notRestoredReasons).toBeNull();
});
Når transferSize === 0 og notRestoredReasons === null, blev siden gendannet fra bfcache. Ethvert andet udfald betyder enten en frisk load eller en blokerende grund, og så får du den eksakte årsag i notRestoredReasons-træet.
Hvorfor bliver min side ikke gemt i bfcache?
Der findes cirka 30 dokumenterede blokerende årsager på tværs af browsere, men i praksis dækker fem stykker over 95 % af alle ineligibility-tilfælde jeg ser:
Cache-Control: no-store på HTML-dokumentet. Browsere nægter at bfcache et dokument der eksplicit beder om ikke at blive gemt. Det er den nummer ét årsag på sites med ældre security-headere kopieret fra OWASP-cheatsheets der ikke har skelnet mellem HTML og API-svar.
Registrerede unload-event-handlers. Chrome og Firefox behandler enhver window.addEventListener('unload', ...) som diskvalificerende, fordi handlere historisk har antaget, at siden destrueres ved navigation. Brug pagehide i stedet.
Åbne WebSocket- eller WebRTC-forbindelser. Browseren kan ikke fryse en aktiv socket, så enten lukker du den i pagehide, eller du accepterer at den side er bfcache-ineligible.
IndexedDB-transaktioner der ikke er afsluttet. Hvis en transaktion er åben når siden forlades, blokerer den. Commit eller abort før pagehide.
Tilladelser brugt aktivt (geolocation watch, sensor APIs, MediaStream). Disse holder ressourcer som ikke kan fryses.
I Chrome er der derudover en lang hale af mindre kendte blokerere: BroadcastChannel uden close, SharedWorker-referencer, og inden for de seneste år også SpeechSynthesis-køer der ikke er tomme. Det fulde sæt findes i Chromes officielle bfcache-dokumentation.
Så, for at fjerne den hyppigste blokering (no-store), skal du opdele dine cache-header-regler efter sti og indholdstype. En Nginx-konfiguration der gør det rigtigt:
location ~* \.(?:js|css|woff2|avif|webp|png|jpg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /api/ {
add_header Cache-Control "no-store"; # API-svar må aldrig caches
}
location / {
# HTML: bfcache-venlig, men ikke shared cache
add_header Cache-Control "private, no-cache";
}
no-cache (ikke no-store!) betyder revalidér før brug, men dokumentet må gerne bfcaches. Det er distinktionen næsten alle kommer galt afsted med.
Not Restored Reasons API: fejlfind specifikke navigationer
Siden Chrome 109 og Safari 17.4 har PerformanceNavigationTiming et notRestoredReasons-felt der returnerer et træ-struktureret objekt med præcis hvorfor en bestemt navigation gik glip af bfcache. Det er forskellen mellem at gætte og at vide. Ærligt talt, jeg ramte selv denne præcise bug da jeg shippede et migrationsprojekt sidste år, og den nye API sparede mig sandsynligvis en hel dag i fejlfinding.
// Send not-restored reasons til dit RUM-endpoint
addEventListener('pageshow', (event) => {
const [nav] = performance.getEntriesByType('navigation');
if (nav.type !== 'back_forward') return;
if (event.persisted) {
// Siden blev gendannet fra bfcache (succes)
beacon('/rum/bfcache-hit');
} else if (nav.notRestoredReasons) {
// Hvorfor blev den ikke gendannet?
beacon('/rum/bfcache-miss', {
reasons: nav.notRestoredReasons.reasons.map(r => r.reason),
url: nav.notRestoredReasons.url,
blockedFrames: nav.notRestoredReasons.children?.length ?? 0
});
}
});
function beacon(url, data = {}) {
navigator.sendBeacon(url, JSON.stringify(data));
}
Træet er rekursivt: hvis en blokering stammer fra en iframe, finder du den under children. Det er typisk hvor tredjeparts-embeds (chat-widgets, ad-iframes) skjuler problemet. Main-framet ser rent ud, men et indlejret <iframe> har en unload-handler.
Jeg anbefaler at logge denne metrik per side-template og per land. Du vil opdage at en specifik landingsside har 5 % hit-rate fordi marketing har indsat en Facebook-pixel der bruger unload, mens resten af sitet ligger på 85 %.
pageshow og pagehide: de eneste events der virker
Når siden gendannes fra bfcache, kører DOMContentLoaded og loadikke. Det er det, der gør det hurtigt, men det betyder også at enhver kode du har lagt der, ikke kører ved tilbage-navigation. Den korrekte event-cyklus er:
pageshow kører både ved frisk load og ved bfcache-restore. event.persisted === true indikerer en restore.
pagehide kører både ved navigation væk og når siden går i bfcache. event.persisted === true betyder at siden blev gemt.
visibilitychange med document.visibilityState === 'hidden' er det sikreste tidspunkt at sende analytics-beacons.
Et komplet bfcache-venligt setup ser sådan ud:
// Initialiser kun det der skal være friskt ved hver visning
addEventListener('pageshow', (event) => {
if (event.persisted) {
// Brugeren kom tilbage, opdater dynamisk indhold
refreshCartCount();
refreshTimestamps(); // "for 2 minutter siden" skal genberegnes
}
});
// Send beacon når siden bliver skjult, ikke ved unload
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/analytics', buildPayload());
}
});
// Luk WebSocket før bfcache, åbn igen ved restore
let socket;
addEventListener('pagehide', () => socket?.close());
addEventListener('pageshow', (e) => {
if (e.persisted) socket = new WebSocket('wss://example.com/live');
});
Hvis du i dag har window.onunload = () => navigator.sendBeacon(...) i din codebase, så er det det første der skal væk. Det blokerer bfcache i Chrome og Firefox, og visibilitychange er en mere pålidelig erstatning. Den fyrer også når brugeren skifter fane, hvilket unload aldrig gjorde på mobil.
Tredjeparts-scripts, WebSocket og analytics
Tredjeparts-scripts er den næst-største kilde til bfcache-tab efter no-store-headere. Google Tag Manager, Segment, Mixpanel, Hotjar: næsten alle havde indtil 2024 mindst én version i omløb der brugte unload. De nyeste versioner er bedre, men hvis du loader scriptet direkte fra deres CDN uden at låse en version, kan du opleve at hit-raten falder uden varsel når de udgiver en opdatering.
Min anbefaling: kør tredjeparts-scripts gennem en lokal proxy eller pin dem på en specifik version, og verificer bfcache-status i CI som vist ovenfor. For tunge scripts som chat-widgets overvejer jeg at flytte dem til en web worker via Partytown, hvilket både fjerner main-thread-cost og eliminerer dem som bfcache-blokerere, fordi worker-koden ikke ses som DOM-state.
Til at finde de skyldige hurtigt, åbn en privat browser-session, indlæs siden uden tredjeparts-scripts (via DevTools request blocking), og kør bfcache-testen igen. Hvis siden nu er eligible, ved du at problemet ligger i én af de blokerede ressourcer. Føj dem til ét ad gangen indtil du isolerer den.
Måling af bfcache hit-rate i RUM og CrUX
Lab-tests fortæller dig om siden kan bfcaches; RUM fortæller dig om den faktisk bliver. Den enkleste metrik:
I dashboardet vil du gerne se hit-rate per side-template (forside vs. produktside vs. checkout). En sund e-commerce-side har 80–90 % på produktlister og produktsider, og 0 % på checkout (fordi den sjældent navigeres tilbage til og typisk har no-store af legitime grunde).
I CrUX kan du hente public data via PageSpeed Insights API; feltet hedder navigation_types og giver dig fordelingen af navigate, reload, og back_forward_cache. Hvis du sammenligner dit eget RUM med CrUX og ser stor afvigelse, så er det oftest fordi dit RUM ikke skelner mellem mobile og desktop: bfcache hit-rate er typisk 10–15 procentpoint højere på desktop fordi mobil-Chrome har mere aggressiv memory pressure-eviction.
Bemærk også, at en gendannet side ikke giver nye Core Web Vitals-målinger. LCP, CLS og INP rapporteres ikke ved bfcache-restore, fordi det reelt ikke er en ny load. Det betyder dog ikke at CLS-problemer ved restore er gratis: web-vitals biblioteket (v4+) tracker stadig layout-shifts efter pageshow og rapporterer dem korrekt.
Reelle fejl jeg har set i produktion
Tre konkrete sager fra det sidste års arbejde, alle med en før/efter måling.
Sag 1, stort dansk medie, hit-rate fra 12 % til 78 %. Årsag: Cache-Control: no-store, max-age=0 sat globalt af en gammel security-middleware i deres CDN. Efter at have flyttet headeren til kun at gælde /api/* og /admin/*, steg hit-raten over en uge til 78 %. INP p75 faldt fra 320 ms til 180 ms målt over hele trafikken, udelukkende fordi en stor del af visninger nu var bfcache-restores med 0 ms interaction-latency.
Sag 2, SaaS-dashboard, 0 % hit-rate. Årsag: en hjemmebygget beforeunload-handler der spurgte "er du sikker på du vil forlade siden?" hvis der var ugemte ændringer. Handleren var registreret unconditionally ved hver page load. Løsning: registrér kun handleren dynamisk når der faktisk er ugemte ændringer, og fjern den når formularen gemmes. Hit-rate steg til 71 %.
Sag 3, e-commerce, hit-rate stabilt på 88 % undtagen på checkout-success-siden. Det viste sig at være et bevidst valg: checkout-success returnerer no-store for at forhindre at brugeren ser sin ordre ved at trykke tilbage. Det er korrekt sikkerhedsadfærd, og bfcache-miss her er ikke et problem. Pointen: ikke alle ineligible sider er fejl. Mål per template, ikke globalt.
Ofte stillede spørgsmål
Hvor lang tid bliver en side i bfcache?
Chrome holder sider i op til 10 minutter eller indtil memory pressure tvinger eviction. Firefox bruger en LRU-cache med op til 3 sider per fane. Safari har en hård grænse på 30 minutter på desktop og kortere på iOS.
Påvirker bfcache mine analytics-tal?
Ja. Hvis du tæller page views ved DOMContentLoaded, går du glip af bfcache-restores. Flyt tællingen til pageshow og inkludér event.persisted som dimension, så du kan skelne mellem friske loads og restores i din rapportering.
Virker bfcache med Single Page Applications?
Ja, men kun for hård navigation mellem distinkte URL'er (full document loads). In-app navigation via history.pushState gemmes ikke i bfcache, fordi den kun manipulerer den eksisterende side. For SPA-flows brug i stedet Speculation Rules API til prerender.
Skal jeg fjerne Cache-Control: no-store fra HTTPS-sider med personlig data?
Nej. Bfcache er per-fane og lever kun i den brugers egen browser-session, så der er ingen risiko for cross-user-lækage. Men hvis du er bekymret for at en delt enhed viser den forrige brugers data ved tilbage-tryk efter logout, skal du eksplicit kalde history.replaceState eller invalidere session-state ved pagehide.
Hvordan ved jeg om Safari bruger bfcache for min side?
Safari understøtter notRestoredReasons fra version 17.4, men eksponerer kun en summarisk grund (ikke det fulde træ som Chrome). For dybere diagnostik åbn Safari Web Inspector, gå til Network-fanen, naviger tilbage, og kig efter "(from disk cache)" eller fraværet af nye requests. Det indikerer en bfcache-restore.
Lær hvordan du reducerer Time to First Byte (TTFB) under 200 ms i 2026 med edge-caching, HTTP/3, Early Hints og moderne komprimering. Inkluderer praktiske kodeeksempler til Next.js, Cloudflare Workers, Caddy og Nginx, plus en diagnose-tjekliste når TTFB pludselig stiger.
Komplet guide til resource hints i 2026: hvornår preload, preconnect, dns-prefetch og fetchpriority hjælper LCP, og hvordan du måler effekten i feltdata.
Lær hvordan Speculation Rules API kan reducere LCP til næsten nul gennem prerender og prefetch. Praktisk guide med eagerness-niveauer, dokumentmønstre, Chrome DevTools-fejlfinding og produktionsklar konfiguration for 2026.