skip to content

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%

answer

  1. hidden does not mean running
  2. paused in memory versus gone from memory
  3. tab entry outlives the document
  4. one boolean read at startup
  5. Chromium-only, degrade cleanly

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.

solid answer

~60 s

A hidden page is not necessarily a running page. Chromium may **freeze** it: the page stays in memory but its task queues are suspended, so timers, network callbacks and message handlers stop firing until the browser resumes it. It may also **discard** it under memory pressure: the document is unloaded completely, the tab strip entry remains, and revisiting the tab triggers a fresh load. Chromium's Page Lifecycle API surfaces both — a `freeze` event and a `resume` event fire on `document`, and after a discard the new page load sees `document.wasDiscarded === true`. Practically: never assume work started while hidden will finish, because you may be frozen mid-flight; do all persistence at the hidden transition rather than in `freeze`, which is a courtesy signal and not universally implemented; and on startup check `wasDiscarded` so you can restore scroll position and form state instead of showing the user a blank default page they did not ask for. Firefox and Safari do not expose these events, so the design must degrade cleanly without them.

code

javascript · 18 lines
javascript
if (document.wasDiscarded) {
  const saved = localStorage.getItem('session-state');
  if (saved) restore(JSON.parse(saved));
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    localStorage.setItem('session-state', JSON.stringify(snapshot()));
  }
});

document.addEventListener('freeze', () => closeIdleConnections());
document.addEventListener('resume', () => reconcileWithServer());

function snapshot() { return { scrollY: window.scrollY }; }
function restore(state) { window.scrollTo(0, state.scrollY ?? 0); }
function closeIdleConnections() {}
function reconcileWithServer() {}

go deeper

for a junior

Know the headline: a background tab is not guaranteed to keep running, and the browser may pause it or drop it entirely to reclaim memory, so nothing in memory is safe.

for a middle

Distinguish frozen (in memory, task queues suspended, resumable) from discarded (document unloaded, next visit is a fresh load) and name the Chromium signals: freeze and resume on document, plus document.wasDiscarded.

for a senior

Show the design consequence: no work started while hidden is guaranteed to finish, persistence belongs on the hidden transition rather than on freeze, and wasDiscarded should drive a restore path instead of a default render.

for a principal

Decide what the product owes a user returning to a long-idle tab — what is restored, what is re-fetched, what is deliberately forgotten — and make that a stated contract rather than emergent behaviour that differs per browser.

## Why there are more states than visible and hidden Once a page is hidden, keeping it fully alive costs memory. Browsers therefore added intermediate treatments between "running normally" and "gone", and Chromium named them explicitly. **Frozen.** The page's document, DOM and JavaScript heap all still exist, but the browser stops servicing its task queues. Timers do not fire. Fetch callbacks do not run. Message handlers do not run. The page is a paused photograph: everything it had is still there, and nothing progresses. Later the browser may resume it, and execution continues where it left off — from the page's own point of view, a very long time simply passed between two lines of code. **Discarded.** Under real memory pressure the browser goes further and unloads the document entirely. The tab remains in the tab strip with its title and URL, but there is no page behind it. When the user clicks back to that tab, the browser performs a fresh load of the URL. Everything in memory — component state, scroll position, an unsent form — is gone unless it was persisted somewhere durable. ## The signals Chromium exposes these through the Page Lifecycle API: ```js document.addEventListener('freeze', () => { // last courtesy call before task queues are suspended }); document.addEventListener('resume', () => { // the page is running again after having been frozen }); if (document.wasDiscarded) { restoreFromStorage(); // this load replaces a discarded page } ``` `freeze` and `resume` fire on `document`. `document.wasDiscarded` is a boolean, readable during the new page load, that tells you this load exists because the previous page for this tab was discarded. Firefox and Safari implement none of these, so treat them as an enhancement layer, never as your only path. ## What this changes about how you write code **Do not start work while hidden and assume it completes.** A fetch kicked off just before the page freezes may have its response callback deferred indefinitely, and a discard means it will never be delivered at all. Anything with a deadline should either finish before the hidden transition or be resumable from persisted state. **Do not treat `freeze` as your save point.** It is tempting, because it sounds like the precise last moment. But it is Chromium-only, it does not fire on every path to death (a discard can follow a freeze, and an OS-level process kill precedes nothing), and by the time it arrives the page has already been hidden — which was your guaranteed signal. Persist on hidden; use `freeze` at most for extra tidying such as closing a connection you would rather not hold open. **Do use `wasDiscarded`.** This is the genuinely valuable half. A user who returns to a tab expecting the article they were reading, the scroll position they had, and the half-typed comment they left, will be annoyed by a pristine home-state render. Reading `wasDiscarded` on startup lets you distinguish "this is a fresh visit" from "this is a restoration" and rehydrate accordingly, using state you saved at the hidden transition. **Do not hold resources you cannot cheaply re-establish.** A page that is frozen while holding a long-lived connection is a page whose connection is stale on resume — the peer may have given up long ago. Reconciling on resume, or on the transition back to visible, is the honest design: re-check what changed rather than assuming your in-memory view is still true. ## Distinguishing the three ways a page stops running It helps to keep the ladder straight: 1. **Hidden** — still running, just not presented. Timers throttled, no frames produced. 2. **Frozen** — in memory, nothing runs, may resume with all state intact. 3. **Discarded** — not in memory at all, tab entry survives, next visit is a fresh load. Only the first is guaranteed to announce itself everywhere. That asymmetry is the design lesson: the guarantees live at the top of the ladder, so put your durability there and let the lower rungs be optimisations you exploit where they exist. ## What a strong answer sounds like "Hidden does not mean running. Chromium can freeze a hidden page — task queues suspended, state intact, `freeze` and `resume` on `document` — or discard it outright, in which case the next load sees `document.wasDiscarded`. I persist on the hidden transition because that is the portable guarantee, and I use `wasDiscarded` to restore rather than render a fresh default."

  • What happens to a pending fetch when the page is frozen?
    Its callback simply does not run: the page's task queues are suspended, so the response cannot be delivered until the page resumes — and if the page is discarded instead, it never is. Treat any in-flight work at the hidden transition as work that may silently never complete, and make the operation resumable from persisted state.
  • Why not just use the freeze event as your save point?
    Because it is Chromium-only and does not cover every path to death — an operating-system process kill announces nothing at all. The hidden transition already happened before the freeze and is delivered everywhere, so it is the portable guarantee. Use `freeze` for optional tidying such as releasing a connection, not for durability.
  • How does a discarded tab differ from a closed tab, from the user's point of view?
    A closed tab is gone from the tab strip. A discarded tab is still sitting there with its title and URL, so the user believes their page is waiting for them — clicking it triggers a fresh load of that URL. That gap between expectation and reality is exactly what `document.wasDiscarded` lets you close by restoring saved state.

saying these in an interview costs you the question

  • Assumes a hidden page keeps running normally
  • Uses the freeze event as the primary save point
  • Thinks a discarded tab preserves in-memory state
  • Expects freeze and resume to exist in every browser
  • Assumes an in-flight request finishes after the page is backgrounded

context