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 view | Navigation type | Start time of the page view |
|---|---|---|
| The load of the page | navigate, reload, back-forward, prerender or restore | 0, the time origin of the page |
| A restore from the back/forward cache | back-forward-cache | The timeStamp of the pageshow event |
A soft navigation, only with softNavigations: true | soft-navigation | The start of the interaction that caused the navigation |
The navigation type of a load comes from these rules, in the sequence of web-vitals:
prerender, if the browser prerendered the page (document.prerendering, or anactivationStartabove 0).restore, if the browser discarded the page before this load (document.wasDiscarded).- The type of the Navigation Timing entry:
navigate,reloadorback-forward. 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, inperformance.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 thepageshowevent.
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.
| Vital | A load | A restore or a soft navigation |
|---|---|---|
| INP | The 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. |
| CLS | The 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. |
| LCP | The 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. |
| FCP | The 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. |
| TTFB | responseStart 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,frozenorterminated. The checkpoint atterminatedis 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-cacheConnect 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).
| Metric | Kind | Unit | Attributes | Description |
|---|---|---|---|---|
lag_web_vital_inp_histogram | Histogram | ms | navigation_type | Interaction to Next Paint (INP) for each page view. |
lag_web_vital_cls_histogram | Histogram | 1 | navigation_type | Cumulative Layout Shift (CLS) for each page view, in browsers that have layout-shift entries. |
lag_web_vital_lcp_histogram | Histogram | ms | navigation_type | Largest Contentful Paint (LCP) for each page view. |
lag_web_vital_fcp_histogram | Histogram | ms | navigation_type | First Contentful Paint (FCP) for each page view. |
lag_web_vital_ttfb_histogram | Histogram | ms | navigation_type | Time 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_vitalevent has the value and thedeltasince the previous event of the same vital. The latest event for eachbrowser.web_vital.idhas 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
contextmessage withlag.page_view.id. The worker adds the context to each hang report and to each record of the hang journal. The functionpageContext, which you can give tocreateBrowserDeps(), adds more attributes, for example the session ID. The context reads this function at the start of each page view. If the function giveslag.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, withinitialize(). Then it sets the key at each new page view, withset("lag.page_view.id", id). Atstop(), 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 IDsetupAllMonitors() 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.