Skip to the content

Follow a web page through the Page Lifecycle API

page-lifecycle-tracker is a small TypeScript library for monitoring and telemetry code. It follows active, passive, hidden, frozen and terminated. Your monitors record their last values before your exporter sends them.

npm install page-lifecycle-tracker

Read the guideRead the API reference

Live: the Page Lifecycle state of this page

This page is active. The page is visible, and it has the focus.

The state diagram of the Page Lifecycle API. The current state of this page is active.Visibleblurfocusvisibilitychangefreezeresumepagehidepagehide (persisted)pageshow (persisted)pagehideactiveVisible, with the focuspassiveVisible, no focushiddenNot visiblefrozenTasks stoppedterminatedUnloaded

Try it

  • Click outside the browser window. Then click in the page again.

    Not done. Expect active to passive, from blur.

  • Go to another tab or app, or minimize the window. Then come back.

    Not done. Expect hidden, then active again, from visibilitychange.

  • Leave this page, then come back with the Back button of the browser.

    Not done. Expect frozen, then a restore from pageshow.

Transitions 0

The transitions of this visit, the newest first
TimeFromToEvent
0.0 sPage loadactiveStart

The state strip of this visit

The states of this visit on a time line.
  • active0.2 s
  • passive0.0 s
  • hidden0.0 s
  • frozen0.0 s
  • terminated0.0 s

Start with a few lines

Get the shared tracker. Read the state, and subscribe to each transition. Each transition has from, to, trigger and the timeStamp of its browser event.

The package is ESM, with TypeScript types and no dependencies. It uses visibilitychange, pagehide, pageshow, freeze and resume, and no unload listener.

Read the full guide

import { getPageLifecycle } from "page-lifecycle-tracker";

const lifecycle = getPageLifecycle(); // one shared tracker for the page
console.log(lifecycle.getState()); // "active", "passive" or "hidden"

// A monitor observes: it records its values first.
lifecycle.subscribe(({ from, to, trigger, timestamp }) => {
  console.log(`${from} to ${to} (${trigger}) at ${timestamp} ms`);
});

// An exporter sends after all observers, in each engine.
lifecycle.subscribe(({ to }) => {
  if (to === "hidden" || to === "frozen" || to === "terminated") exporter.flush();
}, { phase: "export" });

Observers before exporters at pagehide, in Chromium too

At the end of a page, a monitor records its final values, and an exporter sends the last batch. The order of these two listeners decides if the final values get out.

In experiment E1 of the lag project, Chromium started the pagehide listeners of window in the order of registration. Firefox and WebKit started the capture listeners first. Play one pagehide in the four setups.

All steps show.

Your browser: a test on window started the listeners in the order of registration, as Chromium did in experiment E1.

  1. Chromium 145Your browser

    The exporter added its pagehide listener first. Chromium starts the listeners of window in the order of registration.

    1. pagehide
    2. 1ExporterIts pagehide listener sends the batch.LCPCLSINP (not in the batch)
    3. 2TrackerIts capture listener: active to terminated.
    4. 3MonitorObserve subscriber: it records the final INP.

    The exporter sent the batch before the monitor recorded the final INP. The value is lost.

  2. Firefox 146 and WebKit

    The same page. These engines start the capture listener of the tracker first.

    1. pagehide
    2. 1TrackerIts capture listener: active to terminated.
    3. 2MonitorObserve subscriber: it records the final INP.
    4. 3ExporterIts pagehide listener sends the batch.LCPCLSINP

    The batch has the final INP.

  3. Any engine, with phase: "export"

    The exporter has no listener. It subscribes in the export phase, before the monitor subscribes.

    1. pagehide
    2. 1TrackerIts capture listener: active to terminated.
    3. 2MonitorObserve subscriber: it records the final INP.
    4. 3ExporterExport subscriber: it sends the batch.LCPCLSINP

    The batch has the final INP.

  4. Any engine, with handle(event)

    The exporter keeps its listener, and it starts first. It gives the event to the tracker before it sends.

    1. pagehide
    2. 1ExporterIts pagehide listener starts first. It calls handle(event).
    3. 2Trackerhandle(event): active to terminated.
    4. 3MonitorObserve subscriber: it records the final INP.
    5. 4ExporterIt sends the batch.LCPCLSINP
    6. 5TrackerIts capture listener: the event is already handled. No change.

    The batch has the final INP.

Each lane operates the real tracker on a model of window that starts the listeners in the order of experiment E1. The values are examples.

visibilitychange, pagehide, freeze and the bfcache in one API

getState()

The current state. Each call reads document.visibilityState again, thus a timer between the change and its visibilitychange event also gets hidden.

States and transitions
subscribe(listener, { phase })

All observe subscribers of a transition start before all export subscribers, whatever the order of the scripts.

The subscriber phases
getPageLifecycle()

One tracker for the page, under a global symbol. Two libraries, or two copies of the package, share it.

The shared tracker
handle(event)

An exporter whose own listener starts first gives the event to the tracker. The tracker handles each event object one time.

Capture phase and listener order
mark() and resolve()

Ask which transitions occurred in a measurement window. summarizeTransitions() gives flags, for example wasHidden.

Marks
Back/forward cache

A restore from the bfcache is always a transition with the trigger pageshow, also when the state was visible already.

Back/forward cache restores

Read the documentation

page-lifecycle-tracker

A TypeScript library for the Page Lifecycle API, under the MIT license. It came from lag, a monitor of the lag of the main thread of the browser.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site, under the direction of the author. The tests and an STE linter examine them.