How would you measure, for real users, what share of your site's back navigations are served from the browser's back/forward cache — and what do those restores do to your Core Web Vitals field numbers?
answer
- a ratio needs the right denominator
- back_forward does not mean restored
- segment by template, not sitewide
- restores re-report vitals from the restore point
- pass rate can rise with no load change
basics
~20 sMeasure it in RUM as a ratio — restored back navigations over all back navigations, segmented per template. Restores re-report Core Web Vitals from the moment of restore, so they land in field data as very fast samples that can flatter an unsegmented average.
solid answer
~50 sThe metric is a ratio, so you need both halves. The denominator is history navigations: `PerformanceNavigationTiming` reports `type` as `"back_forward"` for a back or forward navigation, whether or not the page was restored. The numerator is the ones actually restored — your RUM SDK, or the `web-vitals` library, already distinguishes a restored page view and re-reports metrics after a restore, and in Chromium `notRestoredReasons` on the same navigation entry tells you a navigation was rebuilt instead, and why. Report the ratio segmented by template and device, because a sitewide average hides the listing pages where back navigation actually happens. On the vitals side, a restore is measured from the restore point, so its LCP and layout shift are typically near zero and its INP reflects an already-warm page. Those samples are legitimate — the user really did get a fast page — but if the dashboard counts them without segmentation, a rising hit rate lifts your pass rate for reasons unrelated to load performance.
code
javascript · 7 linesconst [nav] = performance.getEntriesByType('navigation');
if (nav && nav.type === 'back_forward') {
// 'back_forward' means a history navigation - restored or not.
const reasons = nav.notRestoredReasons ?? null;
console.log('history navigation, rebuilt:', reasons !== null, reasons);
}go deeper
Know that the browser reports a navigation's type, that back and forward navigations report back_forward, and that this alone does not tell you whether the page was restored.
Explain the metric as restored history navigations over all history navigations, and where each half comes from — the navigation timing entry for the denominator, your RUM or vitals library for the restores.
Show that you would segment by template and device before quoting a number, and that you would keep restore samples separate from cold loads so a rising hit rate cannot mask a real load regression.
Own how this number is reported upward: state its browser coverage limits, keep restores and cold loads legible as distinct segments, and alert per template so a silent eligibility regression surfaces quickly.
## Define the metric before instrumenting it "Back/forward cache hit rate" only means something once you fix the denominator. The useful definition is: of all navigations that were back or forward navigations, what fraction were served by restoring the page? Two wrong denominators are common. Dividing by *all* page views produces a number that moves whenever your traffic mix changes, so a campaign full of first-time visitors makes your "hit rate" look worse with nothing changed. Counting only restores, with no denominator at all, gives an absolute number that rises with traffic and says nothing about eligibility. ## The denominator: identifying a history navigation The browser reports the navigation kind on the navigation timing entry. `performance.getEntriesByType('navigation')[0].type` returns one of `"navigate"`, `"reload"`, `"back_forward"` or `"prerender"`. A back or forward navigation reports `"back_forward"` **whether or not the page was restored** — this is the single most common misreading of the API. Seeing `"back_forward"` tells you the user pressed Back; it does not tell you they got an instant page. Treat it as the denominator only. ## The numerator: identifying an actual restore A restored page did not execute a fresh page load, so you cannot detect it with ordinary load instrumentation — code that only runs at startup never runs again. Two practical sources: - **Your RUM SDK or the `web-vitals` library.** Both already handle the restore case: a restored page is treated as a new measurement occasion and metrics are reported again. If your SDK surfaces that as a flag or a separate report, you have the numerator without writing anything. - **`notRestoredReasons`** on the Chromium navigation timing entry, which reports why a document was *not* restored. A history navigation carrying blocking reasons was rebuilt — and it tells you why, which is why the same instrumentation feeds the eligibility audit. This is Chromium-only as of 2025, so segment reporting by browser rather than presenting one blended number. ```js const [nav] = performance.getEntriesByType('navigation'); if (nav && nav.type === 'back_forward') { // History navigation: this is a denominator sample. const reasons = nav.notRestoredReasons ?? null; report('history-navigation', { rebuilt: reasons !== null }); } ``` ## Segment or the number lies A sitewide hit rate is close to useless. Back navigation is concentrated in a few journeys — search results, category listings, feeds — and those templates are usually the ones carrying the heaviest third-party payloads, which is exactly where blockers live. Segment at minimum by template and by device class. A site can show a respectable blended rate while the listing page that drives its revenue restores almost never. ## What restores do to your field vitals Because a restore is treated as a new measurement occasion, the Core Web Vitals are measured again from the restore point. In practice LCP is very small (the content is already there), layout shift is usually near zero, and the responsiveness metric — INP, which replaced FID as a Core Web Vital in March 2024 — is measured against a page whose JavaScript is already warm and whose main thread is quiet. These samples are honest: the user genuinely experienced a fast page. But they change what your aggregate means. The consequence worth stating explicitly: an improving back/forward cache hit rate raises your field pass rate *without any change in how fast your pages load*. That is a real user-experience win and worth having, but reported as one blended distribution you cannot tell it apart from "we made the page faster" — and you may miss a genuine cold-load regression hiding beneath a rising share of restores. The fix is not to discard the restores, which would understate the experience you actually deliver, but to report cold loads and restores as separate segments alongside the blended number so both effects stay legible. ## Close the loop Once the ratio exists it is both a diagnostic and a guard: paired with the blocking reasons it tells you what to fix, and tracked over time it catches the silent regression where someone adds a listener or a header and the restore rate quietly falls. Alert on the ratio per template rather than on the sitewide average, because per template is where it actually moves.
- Why can't you detect a restore with the same startup code that reports a normal page load?Because nothing restarts. A restored document was never torn down, so module top-level code, framework bootstrap and any "on load" handler you registered do not run a second time. Detection has to come from a signal the browser raises specifically for the restore, which is what RUM SDKs and the web-vitals library already listen for.
- Should you exclude bfcache restores from your Core Web Vitals reporting?No — excluding them understates the experience you actually deliver, since those users really did get an instant page. Report them, but keep cold loads and restores as separate segments alongside the blended number. Otherwise a rising restore share can lift the aggregate while cold-load performance quietly regresses underneath it.
- Your blended hit rate is 55% and leadership is happy. What would you check before agreeing?The per-template split. Back navigation concentrates in listing and search results, and those templates usually carry the heaviest third-party code, so they are the likeliest to be ineligible. A healthy blended figure often comes from low-traffic pages restoring reliably while the revenue-driving listing template almost never does.
saying these in an interview costs you the question
- Treats every back_forward navigation as a cache hit
- Divides restores by all page views instead of history navigations
- Reports one blended hit rate with no per-template breakdown
- Says restores cannot be measured because no page load happens
- Claims a rising pass rate proves the page load got faster