Bfcache optimalizálás 2026: azonnali vissza-előre navigáció és a NotRestoredReasons API
Hogyan mérd és javítsd a bfcache hit rate-et 2026-ban: NotRestoredReasons API, Cache-Control: no-store, unload vs pagehide, WebSocket és IndexedDB kezelés, mezei RUM adatok és CrUX lekérdezés lépésről lépésre.
A bfcache (back/forward cache) optimalizálás azt jelenti, hogy az oldalt teljes memóriabeli állapotával (DOM-mal, JavaScript-hívási vermmel, sőt a görgetési pozícióval együtt) tárolja a böngésző, hogy a Vissza/Előre gombra kattintás nulla hálózati kérés nélkül, ~0 ms alatt visszatöltse. Ha az oldal bfcache-be kerül, az LCP gyakorlatilag azonnali, az INP nem is mérődik újra, a CLS pedig eltűnik, ezért 2026-ban a bfcache hit rate a legolcsóbb Core Web Vitals nyeremény, amit egy nagy forgalmú oldal elérhet. Őszintén szólva, ez az egyetlen olyan optimalizáció, amit egy hétvégén be tudsz vezetni, és a következő CrUX riportban látni fogod. Ebben a cikkben végigmegyünk azon, hogyan derítsd fel a blokkolókat a NotRestoredReasons API-val, hogyan tedd kompatibilissé az analytics- és WebSocket-kódot, és hogyan mérd az egészet valós felhasználói adatokkal.
A bfcache hit gyakorlatilag ingyenes Core Web Vitals javulást ad: az LCP és FCP ~0 ms lesz, az INP újramérése nem történik meg, a CLS pedig zéró.
A Chrome 108+ óta elérhető NotRestoredReasons API pontosan megmondja, miért nem került az oldal bfcache-be, kliensoldalról, RUM-ban is naplózható.
A leggyakoribb blokkoló a Cache-Control: no-store válaszfejléc, a nyitott WebSocket/IndexedDB transaction, valamint a legacy unload eseményfigyelő.
A Firefox 2026-ra kiterjesztette a bfcache-t Cache-Control: no-store mellett is, ha nincs érzékeny fejléc, a Chrome pedig ezt kísérleti flag mögött adja.
Analytics jelentést mindig visibilitychange → hidden vagy pagehide eseményen küldj, sose unload-on: az utóbbi mind Safariban, mind Chromiumban letiltja a bfcache-t.
A CrUX navigation_types dimenziójában a back_forward_cache arány a mezei bfcache hit rate, ezt monitorozd, ne a lokálisan mért százalékot.
Mi az a bfcache és hogyan hat a Core Web Vitalsra?
A bfcache egy memóriabeli pillanatfelvétel az oldalról abban a pillanatban, amikor a felhasználó továbbnavigál. A böngésző nem dobja el a DOM-ot, a JavaScript heap-et, a betöltött subresource-okat, sőt még a <video> lejátszási pozíciót sem: csak felfüggeszti a scriptek futását és eltárolja az egész állapotot. Amikor a felhasználó a Vissza (vagy Előre) gombra kattint, ez a snapshot visszaáll. Nincs hálózati kérés, nincs render, nincs re-hydratáció.
Ennek Core Web Vitals-hatása drámai. A Chrome telemetriája szerint egy bfcache-restore átlagosan 6–10x gyorsabb, mint a második leggyorsabb navigációs típus (a HTTP disk cache-ből érkező). A LCP optimalizálás gyakorlati útmutatójában részletezett minden trükk (prioritás-hint, preload, kritikus CSS) együtt sem éri utol azt, amit egy sikeres bfcache-restore ingyen ad. A Google 2024 óta publikálja a navigation_type = back_forward_cache dimenziót a CrUX-ban, és ez a szegmens szinte mindig 0 ms LCP-vel jelenik meg.
Az INP szempontjából még érdekesebb: a bfcache-ből visszaálló oldal ugyanaz a JS heap, mint amit a felhasználó elhagyott. Nincs újrahidratáció, nincs harmadik fél script inicializálás, nincs framework mount, tehát a "visszalépés utáni első kattintás" INP-je is jellemzően < 20 ms. Egy hírportálnál, ahol a felhasználók fele visszalép a listára, a bfcache hit rate 10%-os emelése többet ér, mint egy komplett bundle-refaktor. Ezt egy előző projekten a saját szememmel láttam: 22-ről 41%-ra tornáztuk fel a hit rate-et, és az INP p75 fele lett anélkül, hogy egy sor React kódot módosítottunk volna.
Hogyan teszteld a bfcache állapotot Chrome-ban és a mezőn?
Lokálisan három eszközt használok, ebben a sorrendben. Egyik sem helyettesíti a másikat, mert a bfcache-viselkedés böngésző-, session- és operációs rendszer-függő.
1. Chrome DevTools Application → Back/forward cache panel. Ez a legelső ellenőrzésed. Nyisd meg az oldalt, navigálj el (pl. about:blank-ra), majd nyomd meg a "Test back/forward cache" gombot. A panel megmondja, hogy az oldal bfcache-elhető-e, és ha nem, felsorolja a blokkoló okokat kategorizálva: "Actionable" (te tudod javítani), "Pending Support" (Chrome még nem támogatja), "Circumstantial" (session-függő).
2. Real-life navigáció Incognito + throttled hálózaton. A DevTools panel néha optimista. Egy második tab megnyitása, valós hálózati késleltetés, vagy egy nyitott WebSocket-kapcsolat miatt a valódi navigáció más eredményt adhat. Én mindig kézzel is végigmegyek: oldal betölt, link kattintás, hardware Vissza gomb.
3. Mezei RUM adat. A lokális teszt csak egy adatpont. A valódi bfcache hit rate a mezőn dől el, mert az attól függ, hogy a felhasználók egyáltalán használják-e a Vissza gombot, milyen böngészőt futtatnak (a Safari szigorúbb), és milyen memórianyomás alatt van az eszközük. Erre lentebb, a hit rate mérés szekcióban mutatok konkrét kódot.
A NotRestoredReasons API a gyakorlatban
A NotRestoredReasons API a Chrome 108-ban shipelt, és 2026-ra a Chromium-alapú böngészők mind támogatják. Firefox 128-ban a szemantika stabilizálódott, Safari-ban részleges támogatás van (16.4+). Az API a PerformanceNavigationTiming objektumon keresztül érhető el:
// Fusson a pageshow eseményben, ne a load-ban:
// a pageshow biztosan tüzel bfcache-restore-nál is.
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Ez már bfcache-restore - a lekérdezés a KORÁBBI navigációra vonatkozik.
return;
}
// Friss navigáció - kérjük le, miért nem álltunk vissza a bfcache-ből.
const navEntry = performance.getEntriesByType('navigation')[0];
const notRestored = navEntry?.notRestoredReasons;
if (notRestored) {
// A struktúra: { blocked, reasons: [{reason: 'unload-handler', ...}], children: [...] }
console.log('Bfcache blokkolók:', flatten(notRestored));
// Küldjük RUM-ba - lásd a hit rate szekciót.
reportToRum('bfcache-blocked', flatten(notRestored));
}
});
function flatten(node, path = 'main') {
const out = (node.reasons ?? []).map((r) => ({ frame: path, reason: r.reason }));
for (const child of node.children ?? []) {
const childPath = child.src ? `${path} > ${new URL(child.src).host}` : `${path} > ?`;
out.push(...flatten(child, childPath));
}
return out;
}
Fontos, hogy az API az előző navigáció bfcache-alkalmasságáról tájékoztat. Vagyis ha most töltődött be az oldal (nem bfcache-restore), akkor a notRestoredReasons arra vonatkozik, miért nem került az előző oldal snapshot-ként cache-be. Ez elsőre furcsa (nekem is egy jó órámba került, mire leesett), de logikus: a döntést a böngésző akkor hozza meg, amikor elhagyod az oldalt.
A children tömb minden iframe-et külön ad vissza. Ha egy cross-origin iframe-hez nem tartozik reasons, csak egy blocked: true, az azt jelenti, hogy a Chrome nem árulja el a részleteket: ez adatszivárgás-védelem. Ilyenkor kérd meg a harmadik felet, hogy hozzáadja a Reporting-Endpoints és a NEL headert, ami engedélyezi a részletes visszajelzést.
A Cache-Control: no-store és a bfcache konfliktusa
Történelmileg a Chrome és a Safari kizárta a bfcache-ből azokat az oldalakat, amelyek Cache-Control: no-store fejlécet küldtek. Ez az egyetlen legelterjedtebb blokkoló, főleg bejelentkezett felületeken (banki, admin, SaaS) alap. 2026-ban a helyzet változóban van:
Firefox 124+: alapból bfcache-eli a no-store-os oldalakat is, kivéve ha érzékeny fejléc (pl. Cache-Control: private, no-store + Authorization) is jelen van, vagy a felhasználó explicit kijelentkezett.
Chrome 129+: kísérleti flag mögött (chrome://flags/#back-forward-cache-no-store) engedélyezi, de az entry lejár 3 percen belül, és Cookie/HTTPOnly változásra automatikusan invalidálódik.
Safari 17+: már régóta bfcache-eli a no-store-os oldalakat, de csak "same-document" visszalépésre.
A gyakorlatban ez azt jelenti, hogy a Cache-Control: no-store már nem automatikus halálos csapás, de a Chrome-nál még mindig a legnagyobb hit rate-nyerő, ha át tudod írni Cache-Control: no-cache, private-re. A no-cache ugyanúgy megköveteli a revalidációt, de nem tiltja a memóriában tartást.
Miért nem áll vissza az oldal a bfcache-ből? A leggyakoribb blokkolók
Az én RUM adataimban 2025 harmadik negyedéve óta a következő tíz ok fedi le a bfcache-hibák 90%-át (Chrome, top-1M-es oldalak mintája):
Cache-Control: no-store: ~38% (Chrome).
unload eseményfigyelő: ~14%. Jellemzően legacy analytics vagy régi jQuery plugin.
Nyitott WebSocket kapcsolat: ~9%. Chat widgetek, real-time dashboard.
Nyitott IndexedDB transaction: ~6%. Nem lezárt readwrite tranzakció.
Aktív BroadcastChannel / SharedWorker referencia: ~5%.
WebLock nem feloldva: ~4%.
Cross-origin iframe blokkolása: ~4%.
Fetch/XHR folyamatban a pagehide pillanatában: ~3%. Klasszikus beforeunload-ban indított sync request.
WebRTC PeerConnection nyitva: ~2%.
Password manager extension interakció: ~2%. Ez circumstantial, nem javítható kódból.
A prioritás-sorrend világos: először unload-ok, aztán Cache-Control, aztán a hosszan élő kapcsolatok. A hosszan élő kapcsolatokat nem kell megszüntetned, csak felfüggesztened a pagehide eseményen, és újranyitnod a pageshow eseményen, ha event.persisted === true.
unload vs pagehide: miért kell most váltanod
Az unload esemény bfcache-ölő. Nemcsak azért, mert Chrome/Safari explicit letiltja a bfcache-t, ha window-n regisztrált unload handler van, hanem azért is, mert az esemény modern böngészőkben már nem is fut le megbízhatóan mobilon. Az iOS Safari például a tab háttérbe küldésekor egyszerűen kilövi a folyamatot, unload nélkül.
A helyes minta a pagehide és a visibilitychange kombinációja:
// Rossz - blokkolja a bfcache-t.
window.addEventListener('unload', () => {
navigator.sendBeacon('/analytics', payload);
});
// Jó - bfcache-barát és mobilon is megbízhatóbb.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/analytics', payload);
}
});
// Fallback deszktop tab-close esetére.
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
// Az oldal bfcache-be került - NE küldjünk beacont, mert még visszatérhet.
return;
}
navigator.sendBeacon('/analytics/final', payload);
});
A visibilitychange → hidden tüzel mind a tab háttérbe küldésekor, mind navigáció közben, mind mobilon a home gomb megnyomásakor: ez az egyetlen esemény, amire mindig számíthatsz. A pagehide ezt kiegészíti a valódi bezárási esetre. A beforeunload-ot tartsd meg, ha kell (pl. nem mentett űrlap), mert az önmagában nem blokkolja a bfcache-t; csak akkor blokkol, ha a felhasználó ténylegesen látott confirm dialógot.
Bfcache hit rate mérése RUM-mal és CrUX-szal
A lokálisan mért bfcache-alkalmasság csak egy proxy. A valódi metrika a bfcache hit rate: hány százaléka a valós Vissza/Előre navigációknak restorál sikeresen. Ezt kétféleképp mérheted.
CrUX Query (nyilvános). A BigQuery-ben elérhető chrome-ux-report.all.202607 tábla tartalmazza a navigation_types dimenziót:
SELECT
origin,
SUM(IF(nav.name = 'back_forward_cache', nav.density, 0)) AS bfcache_share,
SUM(IF(nav.name = 'navigate', nav.density, 0)) AS fresh_navigate_share,
SUM(IF(nav.name = 'back_forward', nav.density, 0)) AS non_cached_back_forward
FROM `chrome-ux-report.all.202607`, UNNEST(navigation_types.entries) AS nav
WHERE origin = 'https://pelda.hu'
GROUP BY origin;
Az igazi mutató a bfcache_share / (bfcache_share + non_cached_back_forward): vagyis az összes Vissza/Előre navigációnak hány százaléka volt bfcache-restore. Az iparági medián 2026 nyarán 45% körül van, a top decilis 70% felett. Ha a te oldalad 20% alatt van, ott biztosan no-store vagy unload a bűnös.
Saját RUM (real-time). A RUM-implementációs cikkben bemutatott web-vitals.js attribuáló módban visszaadja a navigation type-ot. Egészítsd ki a NotRestoredReasons API kimenetével:
import { onLCP } from 'web-vitals/attribution';
onLCP((metric) => {
const navType = metric.navigationType; // 'back-forward-cache' vagy 'back-forward' stb.
const isBfcacheHit = navType === 'back-forward-cache';
const notRestored = performance.getEntriesByType('navigation')[0]?.notRestoredReasons;
sendBeacon('/rum', {
metric: 'LCP',
value: metric.value,
navType,
isBfcacheHit,
// Csak akkor releváns, ha friss navigáció - a bfcache-be nem került előző oldal okait naplózza.
blockers: notRestored ? flatten(notRestored).map((r) => r.reason) : null,
});
});
Így a dashboardon két diagramot fogsz látni: (1) a napi bfcache hit rate trendje, (2) top blokkoló okok. Ha a hit rate hirtelen leesik, az attribuciós tömb megmondja, melyik deploy vezette be az unload-ot vagy a no-store-t. Én ezt egy Grafana boardon tartom, és minden release után 15 percig figyelem. A leggyakoribb regresszió: valaki visszahoz egy régi third-party snippetet, és 5%-nyi hit rate elveszik. Pontosan ezt a bugot fogtam el a múlt hónapban egy ügyfélnél, ahol egy régi Optimizely tag került vissza az A/B-teszt miatt.
Analytics, WebSocket és IndexedDB kezelése bfcache mellett
A legnagyobb ellenállás nem a saját kódodból jön, hanem a harmadik felektől. Íme a három leggyakoribb konfliktus és a megoldásuk.
Analytics beacon. A Google Analytics 4 alapból már visibilitychange-en küld, de a Google Tag Manager container gyakran tartalmaz custom tag-eket unload-dal. Ellenőrizd a GTM debug módban, hogy a "Page Unload" trigger sehol nem tüzel-e; ha igen, cseréld "Window Visibility Hidden"-re. Segmentnél a trackWithBeacon opció, Mixpanelnél a track_pageleave beállítás a megfelelő váltás. A harmadik fél szkriptek optimalizálásáról szóló cikk részletesen tárgyalja a facade patternt is, ami itt bónuszként a bfcache hit rate-et is javítja.
WebSocket / SSE. Ne bontsd a kapcsolatot pagehide-on, mert azt visszalépéskor újra kell nyitnod, ami costly. Helyette: a freeze és resume eseményekre figyelj, és a socket kimenő üzeneteit sorold be egy pufferbe. A backend oldalon a kapcsolat élő marad, mert a Chrome nem zárja a TCP socketet a bfcache alatt (max 3 percig). Ha a szerver keepalive timeoutja rövidebb, állítsd 5 percre.
IndexedDB. Sose hagyj nyitott readwrite tranzakciót futni pagehide idejére. A tranzakciót zárd le explicit tx.commit()-tal, vagy warpold Promise-be, ami pagehide-nál eldob. A MDN IDBTransaction.commit dokumentációja pontos ütemezést ad. Én az utolsó projekten ezen égtem meg először: egy háttérben futó IndexedDB sync 12%-nyi hit rate-et evett meg észrevétlenül.
Gyakran ismételt kérdések
Mi a különbség a bfcache és a HTTP disk cache között?
A HTTP disk cache egyes erőforrásokat (HTML, CSS, JS, kép) tárol lemezen a validációs headerek alapján, és minden navigációnál újra kell parse-olni és futtatni a JS-t. A bfcache az egész oldal futásidejű állapotát (DOM + heap + görgetés + timer-ek) tárolja memóriában, és a restore nem futtat semmit újra, ezért nagyságrenddel gyorsabb.
Meddig marad az oldal a bfcache-ben?
Chrome-ban alapból 10 perc, azután automatikusan törlődik. Cache-Control: no-store mellett (kísérleti flag) csak 3 perc. Safari-ban session-hosszan, de memórianyomásra korábban is evictelődhet. A gyakorlatban a felhasználók ~90%-a 30 másodpercen belül visszalép, tehát a limit szinte sose okoz gondot.
A bfcache működik iframe-ekben is?
Igen, de csak akkor, ha az iframe (és minden aliframe) is bfcache-alkalmas. Egy blokkoló cross-origin iframe (például egy reklám unload-dal) az egész főoldalt kizárja a bfcache-ből. A NotRestoredReasons API children mezője pontosan megmutatja, melyik iframe a bűnös.
Hogyan detektáljam a bfcache restore-t JavaScriptből?
A pageshow eseményben ellenőrizd az event.persisted értékét: ha true, akkor bfcache-restore történt. Ekkor újra kell nyitnod a felfüggesztett WebSocket-eket, frissítened a szerveroldali időbélyeget megjelenítő elemeket, és jelentened az analytics-nak a "bfcache_hit" eseményt.
A beforeunload esemény blokkolja a bfcache-t?
Önmagában nem: csak akkor blokkol, ha a felhasználó ténylegesen látott egy "el akarod hagyni az oldalt?" confirm dialógust. Egy beforeunload listener regisztrálása biztonságos, de kerüld a preventDefault() hívást, ha nincs valódi nem mentett munka. Modern böngészőkben az event.returnValue = '' mintát is mellőzheted.
Miért mutat a DevTools "Actionable" okot, ha a kódomban nincs unload?
Szinte biztosan egy harmadik fél scriptet gyanúsíts. A leggyakoribb bűnösök: régi analytics snippetek (Yandex Metrica pre-2023, Adobe Launch), chat widget-ek (régi Intercom, Drift), és néhány A/B tesztelő platform. Nyisd meg a Sources panelt, keress addEventListener('unload' mintára, és sorold fel a találatokat host szerint.
A Long Animation Frames API pontosan megmondja, melyik szkript, layout vagy stílusszámítás okozza az INP lassulást. Működő kód, RUM integráció és böngészőtámogatás 2026-ban.