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.

Frissítve: 2026. augusztus 21.

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 visibilitychangehidden 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):

  1. Cache-Control: no-store: ~38% (Chrome).
  2. unload eseményfigyelő: ~14%. Jellemzően legacy analytics vagy régi jQuery plugin.
  3. Nyitott WebSocket kapcsolat: ~9%. Chat widgetek, real-time dashboard.
  4. Nyitott IndexedDB transaction: ~6%. Nem lezárt readwrite tranzakció.
  5. Aktív BroadcastChannel / SharedWorker referencia: ~5%.
  6. WebLock nem feloldva: ~4%.
  7. Cross-origin iframe blokkolása: ~4%.
  8. Fetch/XHR folyamatban a pagehide pillanatában: ~3%. Klasszikus beforeunload-ban indított sync request.
  9. WebRTC PeerConnection nyitva: ~2%.
  10. 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.

Nadia El-Sayed
A Szerzőről Nadia El-Sayed

Core Web Vitals specialist focused on real-user monitoring. Believes synthetic-only perf testing is a comforting lie.