skip to content

Navigation and Page Lifecycle

You will learn how the browser moves between pages and states — history entries, URLs, back/forward cache, visibility transitions — and how SPA routers ride on top of it. Interviewers ask because every router bug and every lost-unsaved-work bug traces back here.

on this pageshow

explore

questions

page 1 of 2

A user presses the browser's Back button and the previous page appears instantly — scroll position and typed form values intact, and the Network panel shows no request for the document. What is the browser's back/forward cache doing here, and does the page's JavaScript start over?

level: juniorimportance: must knowfreq 50%

answer

  1. Back is undo, not a new load
  2. whole document frozen, not cached bytes
  3. heap, DOM, scroll, form values survive
  4. load fires once per document lifetime
  5. pageshow is the restore signal

basics

~20 s

The back/forward cache keeps the whole page — DOM plus the JavaScript heap — frozen in memory when you navigate away, then restores that snapshot on Back. Scripts do not re-run, and DOMContentLoaded and load do not fire again.

solid answer

~50 s

The back/forward cache (bfcache) is an in-memory snapshot of a **complete, live document**: the DOM, the CSSOM, the JavaScript heap with all its variables and closures, scroll position, form control values and pending timers. When the user navigates away, the browser freezes the page instead of destroying it — the event loop for that document simply stops running tasks. On a back or forward traversal, the browser puts that frozen document straight back on screen: no document request, no re-parse, no script re-execution. `DOMContentLoaded` and `load` fire exactly once in the page's life and do **not** fire again on restore; the signal for a restore is the `pageshow` event with `event.persisted === true`. So any setup you wrote inside a `load` handler runs once and never again, which is why clocks, polling and "fetch fresh data on start" logic silently go stale after a Back.

go deeper

for a junior

Be able to say plainly that a Back navigation can restore a frozen copy of the page instead of loading it again, so scripts do not re-run and the page comes back exactly as the user left it.

for a middle

Explain the mechanics: the document's event loop is paused, the JavaScript heap and DOM survive intact, and pageshow with event.persisted is the only signal that a restore happened rather than a load.

for a senior

Show that you design for both paths — code must be correct on a cold load and on a restore of a page frozen minutes ago — and that you know a cached entry can be evicted for memory or age at any time.

for a principal

Own the tradeoff of keeping pages eligible: it is real, measurable speed, but it means long-lived in-memory state can reappear on screen, so it changes what your team may safely keep in a page and what must be re-validated on restore.

## What problem bfcache solves Before the back/forward cache existed, pressing Back was an ordinary navigation: the browser fetched or revalidated the document, re-parsed the HTML, re-executed every script, re-hydrated the framework and re-issued all the API calls. Users do not think of Back as a navigation — they think of it as undo — so that round trip felt broken. The back/forward cache makes a history traversal look like what users expect: the previous page, instantly, exactly as they left it. ## What is actually stored bfcache does not store bytes of a response. It stores a **live document, paused**. That includes: - the DOM tree and the CSSOM, with any DOM mutations your script had already made, - the entire JavaScript heap for that document: module-level variables, closures, class instances, in-memory caches, framework state, - scroll position of the document and of scrollable elements, - the current values of form controls, including ones the user typed but never submitted, - pending timers and animation state, suspended rather than cleared. This is why it is often described as "freezing" the page. It is not the HTTP cache. The HTTP cache stores response bytes so the network can be skipped; bfcache stores a running document so *everything* — parsing, execution, rendering, data fetching — can be skipped. ## Freeze and restore, step by step When the user navigates away and the browser decides the page is eligible, the document's event loop stops: no tasks, no timers, no `requestAnimationFrame` callbacks, no observer callbacks. Nothing in the page runs while it sits in the cache. The last thing it sees on the way out is a `pagehide` event. On a back or forward traversal to that entry, the browser reinstates the document and fires `pageshow`. The `PageTransitionEvent` passed to that handler carries a `persisted` property, and `persisted === true` means "this is a bfcache restore, not a fresh load". ```js window.addEventListener('load', () => { startClock(); // runs once, never again after a bfcache restore }); window.addEventListener('pageshow', (event) => { if (event.persisted) { startClock(); // runs on every restore too } }); ``` The practical rule: initialisation that must happen *every time the page becomes visible again* cannot live only in `load` or in top-level module code, because neither runs a second time. ## What does not happen On a restore there is **no** request for the document, **no** HTML parse, **no** script evaluation, and **no** `DOMContentLoaded` or `load`. Framework mount hooks do not run again either, because the components were never unmounted — they were frozen mid-life. Nothing was reset, so nothing needs to be rebuilt. ## It is an optimisation, not a guarantee A page can be evicted from the cache at any moment: memory pressure, too many entries, or simply age — Chrome discards bfcache entries after roughly ten minutes. A page may also be ineligible in the first place because of what it does (an `unload` listener, a `Cache-Control: no-store` document response, certain open connections). So correct code must work on **both** paths: a restore, and an ordinary cold load of the same URL. Write the page so a full reload is always acceptable, and treat bfcache as speed on top of that. ## Which navigations qualify Only history traversals in the same tab — the Back and Forward buttons, `history.back()`, `history.forward()`, a swipe gesture, or a keyboard shortcut for those. Clicking a link to a URL you visited earlier is a *new* navigation and gets a fresh document; so is a reload, a bookmark, and a typed address. That distinction matters when you are trying to reproduce the behaviour: you must actually traverse history, not re-request the URL. ## Why interviewers ask bfcache eligibility has become a metric teams are asked to defend, because a restore is the cheapest possible "page load" and directly improves how the site feels. Candidates who only know "Back is fast now" miss the consequence that matters in code review: your page keeps running with state from minutes ago, and any assumption that a visible page was recently initialised is wrong.

  • If nothing re-runs on a restore, how does a page find out it was restored at all?
    Through the `pageshow` event. It fires both after an ordinary load and after a bfcache restore, and `event.persisted` distinguishes them — `true` means the document came out of the back/forward cache. Setup that must repeat on every appearance belongs in that handler, not only in a `load` listener or top-level module code.
  • Is bfcache the same thing as the HTTP cache serving the document from disk?
    No. The HTTP cache stores response bytes; the browser still parses the HTML and re-executes all scripts, so the page starts from zero state. bfcache stores a paused, fully-constructed document — heap, DOM and scroll included — so there is nothing to parse or execute. A page can be HTTP-cacheable and still bfcache-ineligible.
  • Does a bfcache restore ever happen for a link click to a page the user visited earlier?
    No. Only history traversals qualify — Back, Forward, `history.back()`, `history.go()`, or the equivalent gesture. A link click, a reload, a bookmark or a typed URL creates a new navigation entry and gets a freshly loaded document, even if the same URL is already sitting in the cache.

saying these in an interview costs you the question

  • Says the scripts re-run from the top on a Back restore
  • Confuses bfcache with the HTTP disk cache for the document
  • Assumes DOMContentLoaded or load fires again on restore
  • Thinks any repeat visit to a URL uses bfcache
  • Treats bfcache as guaranteed rather than best-effort

context

open as a page

In a browser page, how do you detect that the tab has been hidden, and what values can `document.visibilityState` take?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Listen for the visibilitychange event on document and read document.visibilityState, which is either "visible" or "hidden" (document.hidden is the boolean shorthand). It flips to hidden on tab switches, window minimising, app switching and screen lock.

open as a page

Your page embeds `<iframe id="widget" src="https://widget.example.com">` and you call `document.getElementById('widget').postMessage(data, 'https://widget.example.com')`, which throws a TypeError. What is the correct call, and what does each argument mean?

level: juniorimportance: must knowfreq 55%

basics

~20 s

An iframe element has no postMessage method; its window does. Call iframe.contentWindow.postMessage(data, 'https://widget.example.com') — the first argument is the copied payload, the second is the origin the frame must currently have or the browser silently drops the message.

open as a page

In browser JavaScript, what is the second argument to the URL constructor for, and why does new URL('/checkout') throw while new URL('/checkout', 'https://shop.example.com/cart/items') does not?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The second argument is a base URL that a relative reference is resolved against. A relative string on its own has no scheme or host, so new URL('/checkout') throws a TypeError; with the base it resolves to https://shop.example.com/checkout.

open as a page

A team adds a `window` `'unload'` listener for analytics, and their HTML document is served with `Cache-Control: no-store`. Why does either one on its own stop the browser from putting that page in the back/forward cache, and what else in a page commonly disqualifies it?

level: middleimportance: must knowfreq 52%

basics

~20 s

Both are promises the browser cannot keep while a page is frozen. An unload listener claims code runs at teardown, which a cached page never reaches, and Cache-Control: no-store says the response must not be retained at all — so the browser skips caching and does a full reload instead.

open as a page

In a plain web page, how does your code tell that the document was restored from the browser's back/forward cache rather than freshly loaded, and what does the `persisted` property mean on the `pageshow` and `pagehide` events?

level: middleimportance: must knowfreq 45%

basics

~20 s

Listen for pageshow: it fires after an ordinary load and after a back/forward cache restore, and event.persisted === true marks the restore. On pagehide, persisted === true means the page is going into that cache rather than being discarded.

open as a page

In a vanilla JavaScript router, calling history.pushState({}, '', '/about') updates the address bar but the view never changes and the window's popstate listener never runs. Why does the browser not notify you, and what must the router do instead?

level: middleimportance: must knowfreq 78%

basics

~20 s

history.pushState is deliberately silent: it changes the URL and adds a session-history entry without navigating, without a network request, and without firing popstate or hashchange. Only user traversal fires popstate, so the router must render itself after pushing.

open as a page

Which navigations does the `navigate` event on `window.navigation` fire for, and what does calling `event.intercept({ handler })` inside that listener do?

level: middleimportance: must knowfreq 45%

basics

~20 s

The navigate event on window.navigation fires for navigations the page is involved in — link clicks, form submissions, fragment changes, back/forward traversals, and navigation.navigate() calls. event.intercept({handler}) turns the navigation into a same-document one your handler completes.

open as a page

What are the concrete risks of calling `window.postMessage(data, '*')` with a wildcard `targetOrigin`, and when is `'*'` genuinely acceptable?

level: middleimportance: must knowfreq 65%

basics

~20 s

A wildcard targetOrigin tells the browser to deliver the message no matter which origin the receiving window is on, so if that window navigated or was replaced, the payload lands in a stranger's page. Use '*' only for data that is safe to publish.

open as a page

A page runs `window.addEventListener('message', e => applyConfig(e.data))` to accept settings from an iframe it embeds. Why is that handler a security bug, and what must it check?

level: middleimportance: must knowfreq 70%

basics

~20 s

The window message event is a shared bus: any window holding a reference to yours — every embedded frame, your opener, a popup you opened — can post to it, so this handler applies configuration from strangers. Check event.origin against an exact allowlist, compare event.source to the expected window, and validate the payload's shape.

open as a page

A web app persists the user's unsaved draft in a `beforeunload` handler, with an `unload` handler as a backup, yet mobile users still lose drafts. Why are those two events unreliable, and which lifecycle event is the dependable place to save?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Mobile operating systems can kill a backgrounded tab without ever running unload or beforeunload, so neither is guaranteed. Save on the visibilitychange transition to hidden — the last event browsers reliably deliver — with pagehide as the secondary signal.

open as a page

How do you feature-detect the Navigation API in a browser, and what do you fall back to when it is unavailable?

level: juniorimportance: should knowfreq 28%

basics

~20 s

Test for the object itself with 'navigation' in window before using it. When it is missing, fall back to the History API path: pushState plus a popstate listener, with your own click and submit interception.

open as a page

On the browser's window object, what exactly makes the popstate event fire versus the hashchange event, and does either fire when a script calls history.pushState()?

level: middleimportance: should knowfreq 44%

basics

~20 s

popstate fires when the user traverses session history — Back, Forward, or history.go() — within the same document. hashchange fires when the URL fragment changes through a navigation, such as clicking an anchor link or assigning location.hash. Neither fires for pushState or replaceState.

open as a page

In a client-side router, when should you call history.replaceState() instead of history.pushState(), and what goes wrong if you always push?

level: middleimportance: should knowfreq 58%

basics

~20 s

pushState adds a session-history entry, replaceState overwrites the current one. Use replaceState for high-frequency URL updates such as filters and for redirects; pushing on every keystroke buries the previous page under dozens of entries and breaks the Back button.

open as a page

Inside a `navigate` event listener on `window.navigation`, when is `event.canIntercept` false, and what should your code do in that case?

level: middleimportance: should knowfreq 32%

basics

~20 s

event.canIntercept is false when the browser cannot turn the navigation into a same-document one — a cross-origin destination, a download link, or a cross-document back/forward traversal. Return early and let the browser navigate; calling intercept() then throws.

open as a page

A dashboard refreshes with `setInterval(loadStats, 5000)`. What do browsers do to that interval once the tab has sat in the background for a while, and how should the page handle it?

level: middleimportance: should knowfreq 50%

basics

~20 s

Browsers clamp timers in hidden pages — typically to about once a second, and Chromium drops to roughly once a minute after several minutes hidden. Missed ticks are not replayed. Stop polling on the hidden transition and refetch once when the page becomes visible.

open as a page

What is the difference between the document `visibilitychange` event and the window `focus`/`blur` events, and when is a page visible but not focused?

level: middleimportance: should knowfreq 45%

basics

~20 s

Visibility says whether the document is being presented at all; focus says whether it receives keyboard input. A page beside a focused second window, or with DevTools focused, stays visible while blurred. Use visibility to suspend work, focus for input-related behaviour.

open as a page

Why does new URLSearchParams({ q: 'a b' }).toString() produce q=a+b while encodeURIComponent('a b') produces a%20b, and when does that difference cause a bug?

level: middleimportance: should knowfreq 50%

basics

~20 s

URLSearchParams serialises with the application/x-www-form-urlencoded rules, which encode a space as +, while encodeURIComponent uses generic percent-encoding and produces %20. Bugs appear when a value containing a literal + is concatenated into a query string by hand and read back as a space.

open as a page

Is the object returned by a URL instance's searchParams property a live view of that URL or a detached snapshot, and what breaks in a helper that works on new URLSearchParams(url.search) instead?

level: middleimportance: should knowfreq 45%

basics

~20 s

It is a live view: the same URLSearchParams object is returned every time, and mutating it rewrites the URL's search and href. A helper that builds new URLSearchParams(url.search) gets a detached copy, so its changes are silently lost unless it assigns the result back.

open as a page

A query string is ?tag=new&tag=sale. Using URLSearchParams, what does get('tag') return, and how do append() and set() differ when the key already exists?

level: middleimportance: should knowfreq 58%

basics

~20 s

URLSearchParams keeps every repeated key as its own entry: get('tag') returns only the first value, 'new', and getAll('tag') returns ['new','sale']. append() adds another entry, while set() collapses all existing entries for that key into one.

open as a page

A user clicks Log out, lands on the login page, then presses Back — and the dashboard reappears fully rendered with their name and data, restored from the back/forward cache. Why does the frozen page come back intact, and what are your options for handling state that has gone stale while a page sat in that cache?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A restored page resumes the exact heap and DOM it had when frozen, so already-rendered data reappears without any code running to re-check it. Fix it by serving the sensitive document with Cache-Control: no-store, or by re-validating on pageshow when event.persisted is true.

open as a page

By default, what does a browser do to the scroll position when the user presses Back, and what does setting history.scrollRestoration to 'manual' change for a single-page app?

level: seniorimportance: should knowfreq 32%

basics

~20 s

By default the browser stores each history entry's scroll offset and restores it during traversal — too early for a single-page app that renders only after popstate, so the page is still short and the offset is clamped. Setting history.scrollRestoration = 'manual' hands that job to your router.

open as a page

What are the rules for the state object passed to history.pushState() — how is it stored, what can it hold, and why is history.state null when a page first loads?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The state object is structured-cloned and stored with the session history entry, so it survives reloads and session restore but drops functions, DOM nodes and class prototypes. The entry created by the initial page load carries no state, so history.state is null until replaceState seeds it.

open as a page

A single-page app wants to send the user back to one specific earlier view after a multi-step flow. How do `navigation.entries()`, `navigation.currentEntry` and `navigation.traverseTo()` make that possible, and why is it more reliable than stepping back a fixed number of entries?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Save navigation.currentEntry.key before the flow starts, then call navigation.traverseTo(key) to return to exactly that entry. A fixed number of back steps is unreliable because redirects, replacements and the user's own navigation change how many entries lie in between.

open as a page

You embed a third-party widget in an iframe and exchange dozens of messages per second in both directions. Why set up a `MessageChannel` and hand one port to the frame instead of calling `window.postMessage` with an origin check on every message?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A window's message event is a shared bus every frame and opener can post to, so each message needs revalidation. A MessageChannel gives two entangled MessagePorts; after one verified handover, traffic flows on a private pipe only the two peers hold, with no per-message origin checks.

open as a page

A community site renders user-submitted links with `target="_blank"`. Explain reverse tabnabbing through `window.opener`, and what you would do about it given how browsers behave today.

level: seniorimportance: should knowfreq 45%

basics

~20 s

A page opened with target="_blank" gets a window.opener handle and, even cross-origin, may assign window.opener.location to redirect the tab the user came from to a phishing copy. Add rel="noopener" to user-submitted links, and pass 'noopener' to window.open, which modern anchor defaults do not cover.

open as a page

Your client-side request cache stores responses keyed by URL string, and identical requests keep missing the cache. Using the URL and URLSearchParams APIs, how would you canonicalise a URL into a stable key, and what does the parser normalise for you already?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Parse with the URL constructor, which already lower-cases the scheme and host, drops a default port and resolves dot segments, then finish the job yourself: drop the fragment, sort the query with URLSearchParams.sort(), and remove parameters that do not affect the response.

open as a page

Field data shows most Back navigations to your site are full page loads rather than back/forward cache restores. How do you find out, page by page, why the browser refused to restore each one?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Reproduce it locally in Chrome DevTools' Application panel, which has a Back/forward cache tool that navigates away and back and lists the blocking reasons. For real users, read PerformanceNavigationTiming.notRestoredReasons, available in Chrome 123 and later.

open as a page

Beyond visible and hidden, Chromium browsers may freeze or discard a backgrounded tab. What do those two states mean for a running page, and which document-level signals expose them?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Freezing suspends a hidden page's task queues so no timers, callbacks or handlers run until it resumes; discarding unloads the page entirely, leaving the tab entry to reload on return. Chromium signals them with the document freeze and resume events and document.wasDiscarded.

open as a page

showing 1–30 of 31