Skip to the content

Page views

What a page view is, how each event and each Web Vital belongs to one page view, and how the worker and the crash reports of Chromium get the ID of the page view.

The page view is the unit of analysis of the library, as it is for the Core Web Vitals. When the page-view vitals operate, each event of the monitors has the ID of its page view. Thus you can connect a hang, a long animation frame or a clock jump with the Web Vitals of the same page view. PageViewVitals makes the page views, and the page-view vitals monitor gives its metrics.

What a page view is

PageViewVitals starts a new page view at these times:

Start of the page viewNavigation typeStart time of the page view
The load of the pagenavigate, reload, back-forward, prerender or restore0, the time origin of the page
A restore from the back/forward cacheback-forward-cacheThe timeStamp of the pageshow event
A soft navigation, only with softNavigations: truesoft-navigationThe start of the interaction that caused the navigation

The navigation type of a load comes from these rules, in the sequence of web-vitals:

  1. prerender, if the browser prerendered the page (document.prerendering, or an activationStart above 0).
  2. restore, if the browser discarded the page before this load (document.wasDiscarded).
  3. The type of the Navigation Timing entry: navigate, reload or back-forward.
  4. navigate, if the browser gives no applicable navigation entry.

For a prerendered page, the measurement starts at the activation of the page. The load metrics count from activationStart. Only Chromium prerenders pages, from version 108 (browser support).

A soft navigation is a change of the URL in the same document, after an interaction, with a new contentful paint. Chromium reports soft navigations from version 151. PageViewVitals uses them only when the browser has the entry types soft-navigation and interaction-contentful-paint. The default of softNavigations is false, as the default of web-vitals (Web Vitals algorithms).

Each page view has these properties:

  • id: a random ID.
  • navigationType: one of the navigation types above.
  • startTime: the start of the page view, in performance.now() time.
  • interactionId: for a soft navigation, the interaction that caused it.
  • url: the URL at the start of the page view, without the query string and the fragment. For a restore, it is the URL of the document at the pageshow event.
import { createBrowserDeps, createNoopMeter, setupAllMonitors } from "@mark1russell7/lag";

const monitors = setupAllMonitors(createBrowserDeps(window, {
    logger : { log : (level, message) => console.log(level, message) },
    meter : createNoopMeter(),
    softNavigations : true,
}));

const view = monitors.vitals?.getView();
console.log(view?.id, view?.navigationType);

// Show each new page view: a restore from the back/forward cache or a soft navigation
const unsubscribe = monitors.vitals?.subscribe((next) => console.log(next.id, next.navigationType));

The page-view ID on each event

setupAllMonitors() wraps the event sink with withEventContext(). The wrapper adds the attribute lag.page_view.id to each event, with the ID of the current page view. It reads the ID again for each event, thus an event after a restore gets the ID of the new page view. An attribute of the event itself replaces the attribute of the context with the same name. The wrapper operates only when the page-view vitals operate, thus only where the browser has PerformanceObserver.

These events get the ID in this way: lag.stall, lag.long_animation_frame, lag.clock.jump, lag.browser_report and lag.main_thread.hang. The browser.web_vital event sets lag.page_view.id itself, to the page view that the value belongs to. It also has lag.page_view.url. A lag.main_thread.hang event with the phase abandoned has the ID of the page view that hung, from the hang journal.

The ID is not a metric attribute. Each page view has its own ID, thus the ID has too many values for a metric series. Refer to metrics model.

The Web Vitals of each page view

ViewCollector calculates the vitals of one page view, with the rules of web-vitals. This table is a summary. Web Vitals algorithms gives the full rules and the differences from web-vitals.

VitalA loadA restore or a soft navigation
INPThe longest interaction, with one outlier ignored for each 50 interactions. The candidates are the event entries of 16 ms or more and the first-input entry. The count of the interactions starts at the start of the page. A candidate of 0 ms gives an INP of 0.The same rules, from the start of the page view. If interactions occurred but none has an entry, INP is 8 ms.
CLSThe worst session window of layout shifts without recent input. A shift starts a new session window if it comes 1 s or more after the previous shift, or 5 s or more after the first shift of the window. The value comes only after FCP.The same rules. The value starts at 0.
LCPThe latest largest-contentful-paint entry before the page is hidden for the first time, from activationStart. The first trusted keydown or click after the start of the page view makes the value final.A restore: the time from the pageshow event to the second animation frame callback after it. A soft navigation: the largest interaction-contentful-paint of its interaction, until the first trusted keydown or click.
FCPThe first-contentful-paint entry before the page is hidden for the first time, from activationStart.A restore: the same time as LCP. A soft navigation: the presentation time or the paint time of the soft-navigation entry, from its start.
TTFBresponseStart minus activationStart, from the Navigation Timing entry.0, if the load had an applicable navigation entry.

The page source (createPageSource()) ignores a navigation entry whose responseStart is 0, or is not before performance.now(), as web-vitals does. The rating of each value uses the thresholds of web-vitals. For example, an INP of 200 ms or less is good, and an INP above 500 ms is poor.

A vital whose entry type the browser does not have gets no value, as in web-vitals. INP needs event entries, CLS needs layout-shift entries, LCP needs largest-contentful-paint entries, and FCP needs paint entries. For example, Firefox and Safari give no CLS, also after a restore.

The interaction count comes from performance.interactionCount where the browser has it: Chromium 144, Firefox 144 and Safari 26.2 (browser support). In other browsers, the calculator counts the interactions that it saw. Fast interactions make no entry, thus this count is too low, and INP can be slightly too high on a page with many fast interactions.

Checkpoints and flush

PageViewVitals gives the current values of a page view at each checkpoint. These are the checkpoints:

  • The page changes to hidden, frozen or terminated. The checkpoint at terminated is final.
  • A new page view starts. The checkpoint of the earlier page view is final.
  • stop() makes a final checkpoint.
  • flush() makes a checkpoint that is not final.

After the final checkpoint, the page view has ended, and no report for it comes. For example, a flush() after pagehide makes no second report. A checkpoint that is not final and has no values makes no report.

At each delivery of the browser and before each checkpoint, PageViewVitals also takes the entries that the other observers did not deliver yet. It processes all of them in the sequence of their start times, but it keeps the sequence of the browser for each observer. A soft-navigation entry starts a new page view, and the entries after it in this sequence go to the new page view. As in web-vitals, an entry that the browser delivered before the soft-navigation entry stays in the earlier page view, also if it starts later. The new page view ignores the interaction that caused the soft navigation, also when the browser delivers one of its entries after the soft-navigation entry.

Show the diagram source
sequenceDiagram
    participant B as Browser
    participant V as PageViewVitals
    participant T as Metrics and events
    B->>V: load
    Note over V: view 1, navigate
    B->>V: the page becomes hidden
    V->>T: checkpoint: first values of view 1
    B->>V: the page becomes visible
    B->>V: pagehide, persisted
    V->>T: checkpoint
    B->>V: pageshow, persisted
    V->>T: final checkpoint of view 1
    Note over V: view 2, back-forward-cache
A load, a tab switch, and a restore from the back/forward cache. The final checkpoint of the first page view comes when the second page view starts.

Connect flush() to the exporter, so that the export at a page hide contains the latest values. Refer to the flush hook.

One histogram value for each page view

The histogram of each vital gets one value for each page view: the value at the first report of that vital. Usually, the first report comes when the page becomes hidden for the first time. A histogram cannot remove a value, thus the later changes of the value go only into events. No report comes after the final checkpoint, thus the histogram cannot get a second value for a page view. A CDP test hides a real page two times in Chromium. The second hide records no second FCP value and sends no new event (testing strategy).

MetricKindUnitAttributesDescription
lag_web_vital_inp_histogramHistogrammsnavigation_typeInteraction to Next Paint (INP) for each page view.
lag_web_vital_cls_histogramHistogram1navigation_typeCumulative Layout Shift (CLS) for each page view, in browsers that have layout-shift entries.
lag_web_vital_lcp_histogramHistogrammsnavigation_typeLargest Contentful Paint (LCP) for each page view.
lag_web_vital_fcp_histogramHistogrammsnavigation_typeFirst Contentful Paint (FCP) for each page view.
lag_web_vital_ttfb_histogramHistogrammsnavigation_typeTime to First Byte (TTFB) for each page view. A restore from the back/forward cache and a soft navigation have no network response and get 0, as in web-vitals. A page without a navigation entry gets no value.

This rule has three effects:

  • Each page view counts one time. The count of a vital histogram is the number of page views that have the vital. A quantile of the histogram is a quantile of page views, also for a page view that reports many times.
  • The first value can be lower than the final value. For example, the INP of a page view increases if the user comes back to the tab and makes a slower interaction. The histogram keeps the first value.
  • The events have the final value. Each browser.web_vital event has the value and the delta since the previous event of the same vital. The latest event for each browser.web_vital.id has the final value. An unchanged value sends no new event.

The attribute names of browser.web_vital agree with the OpenTelemetry semantic conventions (v1.44). The attribution, for example the CSS selector of the INP target, goes into the attributes lag.web_vital.*, because the conventions do not have such attributes.

This query gives the INP at the 75th percentile of the page views, for each navigation type:

histogram_quantile(0.75, sum by (navigation_type) (rate(lag_web_vital_inp_histogram[1h])))

The page-view context for the worker and for crash reports

The worker and the browser can report while the main thread cannot operate. Thus they must have the ID of the page view before a hang starts. The page-view context gives the ID of the current page view to them at each new page view:

  • To the worker. The worker monitor sends a context message with lag.page_view.id. The worker adds the context to each hang report and to each record of the hang journal. The function pageContext, which you can give to createBrowserDeps(), adds more attributes, for example the session ID. The context reads this function at the start of each page view. If the function gives lag.page_view.id, the ID of the page view replaces it.
  • To the crash-report context of Chromium. window.crashReport (Chrome 145 and later) keeps keys and values that the browser adds to its crash reports. The library initializes the context one time, with initialize(). Then it sets the key at each new page view, with set("lag.page_view.id", id). At stop(), it removes the key.
Show the diagram source
sequenceDiagram
    participant V as PageViewVitals
    participant C as Page-view context
    participant W as Worker
    participant R as window.crashReport
    V->>C: a new page view starts
    C->>W: context message with lag.page_view.id
    C->>R: set the key lag.page_view.id
    Note over W: each hang report of the worker has the ID
    Note over R: a crash report of an unresponsive page has the ID
The worker and the crash-report context get the ID at each new page view, while the main thread can still operate.

setupAllMonitors() adds the page-view context only when the page-view vitals operate, and only with a worker monitor, a peer hang watch or a crash-report context. createBrowserDeps() gives window.crashReport only if it has a set() function, and only while crashReportContext is not false. If another script initialized the crash-report context first, initialize() fails, but set() still operates.

Chromium sends a crash report, for example for an unresponsive page that the browser stopped, to the reporting endpoint of the page. The endpoint is the crash-reporting endpoint of the Reporting-Endpoints header, or the default endpoint. A script cannot read these reports with a ReportingObserver (browser support). Thus the report gets to your server, and the server can connect it with the events of the page view through lag.page_view.id.

lag: Main-thread responsiveness monitoring for browser apps, exported as OpenTelemetry metrics.

To change a page, edit its file in packages/site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them. The writing standard gives the reason for this note.