Starting with field data: what CrUX reveals about INP

Field data captures how real users actually experience your site, exposing problems that lab testing alone cannot uncover. For Interaction to Next Paint (INP), field data is the only reliable way to identify slow interactions and begin understanding what is causing them.

If you don't yet collect your own field data, the Chrome User Experience Report (CrUX) is a solid starting point. CrUX aggregates telemetry from Chrome users who have opted into data collection and surfaces INP along with other Core Web Vitals across several scopes:

To begin, enter your URL into PageSpeed Insights. If field data is available, you'll see INP values for both mobile and desktop dimensions:

Field data as shown by CrUX in PageSpeed Insights, showing LCP, INP, CLS at the three Core Web Vitals, and TTFB, FCP as diagnostic metrics, and FID as a deprecated Core Web Vital metric.
A readout of CrUX data as seen in PageSpeed insights. In this example, the given web page's INP needs improvement.

CrUX answers whether your site has an INP problem, but it can't tell you what's causing it. For that level of understanding, you need your own field data collection. Real User Monitoring (RUM) solutions are one approach; another is using the web-vitals JavaScript library directly.

Collecting INP data with web-vitals

The web-vitals library lets you gather user field data for INP in supporting browsers. Getting started is straightforward:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  console.log(name);    // 'INP'
  console.log(value);   // 512
  console.log(rating);  // 'poor'
});

To make this data useful, send it to an analytics endpoint:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  // Prepare JSON to be sent for collection. Note that
  // you can add anything else you'd want to collect here:
  const body = JSON.stringify({name, value, rating});

  // Use `sendBeacon` to send data to an analytics endpoint.
  // For Google Analytics, see https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics.
  navigator.sendBeacon('/analytics', body);
});

Standard INP data alone, however, offers little more insight than CrUX provides. This is where the attribution build of the library becomes valuable.

The attribution build: deeper insight into slow interactions

The attribution build of the web-vitals library exposes a wealth of additional information through the attribution object on the onINP() method:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, rating, attribution}) => {
  console.log(name);         // 'INP'
  console.log(value);        // 56
  console.log(rating);       // 'good'
  console.log(attribution);  // Attribution data object
});
How console logs from the web-vitals library appears. The console in this example shows the name of the metric (INP), the INP value (56), where that value resides within the INP thresholds (good), and the various bits of information shown in the attribution object, including entries from The Long Animation Frames API.
How data from the web-vitals library appears in the console.

Using this data, you can answer questions the standard build can't address, including:

  • Did the user interact with the page while it was still loading?
  • Did an interaction's event handlers take an unusually long time to execute?
  • Was event handler execution delayed, and if so, what else was occupying the main thread?
  • Did the interaction produce heavy rendering work that delayed the next frame?
attribution object key Data
interactionTarget A CSS selector pointing to the element that produced the page's INP value—for example, button#save.
interactionType The interaction's type, either from clicks, taps, or keyboard inputs.
inputDelay* The interaction's input delay.
processingDuration* The time from when the first event listener started running in response to the user interaction until when all event listener processing has finished.
presentationDelay* The interaction's presentation delay, which takes place starting from when event handlers finish to the time the next frame is painted.
longAnimationFrameEntries* Entries from the LoAF associated with the interaction. See the next for additional info.
*New in version 4

Since version 4, the web-vitals library also provides INP phase breakdowns (input delay, processing duration, presentation delay) and leverages the Long Animation Frames API for even deeper troubleshooting of problematic interactions.

Leveraging the Long Animation Frames API (LoAF)

Debugging slow interactions via field data has historically been difficult. LoAF changes that by exposing granular timings and source-level information for long animation frames. The attribution build surfaces an array of LoAF entries under longAnimationFrameEntries on the attribution object, and each entry contains valuable diagnostic data:

LoAF entry object key Data
duration The duration of the long animation frame, up to when layout has finished, but excluding painting and compositing.
blockingDuration The total amount of time in the frame that the browser was unable to respond quickly due to long tasks. This blocking time can include long tasks running JavaScript, as well as any subsequent long rendering task in the frame.
firstUIEventTimestamp The timestamp of when the event was queued during the frame. Useful for figuring out the start of an interaction's input delay.
startTime The starting timestamp of the frame.
renderStart When the rendering work for the frame began. This includes any requestAnimationFrame callbacks (and ResizeObserver callbacks if applicable), but potentially before any style/layout work begins.
styleAndLayoutStart When style/layout work in the frame occurs. Can be useful in figuring out the length of style/layout work when figuring in other available timestamps.
scripts An array of items containing script attribution information contributing to the page's INP.
A visualization of a long animation frame according to the LoAF model.
A diagram of the timings of a long animation frame according to the LoAF API (minus blockingDuration).

Among the available LoAF data, the scripts array deserves particular attention because it identifies which scripts contributed to the slow interaction and how:

Script attribution object key Data
invoker The invoker. This can vary based on the invoker type described in the next row. Examples of invokers can be values like 'IMG#id.onload', 'Window.requestAnimationFrame', or 'Response.json.then'.
invokerType The type of the invoker. Can be 'user-callback', 'event-listener', 'resolve-promise', 'reject-promise', 'classic-script', or 'module-script'.
sourceURL The URL to the script where the long animation frame originated from.
sourceCharPosition The character position in the script identified by sourceURL.
sourceFunctionName The name of the function in the identified script.

Diagnosing Slow Interactions from Field Data

Once field data has flagged an interaction as problematic for INP, the next step is understanding what went wrong. The web-vitals library's attribution build exposes Long Animation Frames (LoAF) data that can help pinpoint root causes across the three phases of an interaction: input delay, processing time, and presentation delay.

High Processing Durations

The processing duration measures how long the event handler callbacks take to complete. High values don't always mean your handler code is the culprit—third-party scripts may register their own listeners. The library surfaces this directly:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5
});

If high processing time is confirmed, LoAF data can identify the precise source:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5

  // Get the longest script from LoAF covering `processingDuration`:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.toSorted((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Get attribution for the long-running event handler:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

From here, you can extract the specific element and event listener, the script file (and character position) containing the long-running handler code, and the event listener function name. This removes guesswork from tracing which interaction or handler was responsible. For your own code, consult guidance on optimizing long tasks.

High Input Delays

Input delay is the time between the user's initial action and the start of the event handler callbacks, occurring when the main thread is otherwise occupied. The attribution build exposes this value directly:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536
});

Whether delay happens during page load or after it changes the likely cause and fix.

During Page Load

The main thread is often busiest when a page is loading. Script evaluation and compilation, along with initialization functions, can block the thread just as a user attempts to interact. The invoker types in LoAF data can confirm this case:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.toSorted((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Invoker types can describe if script eval blocked the main thread:
    const {invokerType} = script;    // 'classic-script' | 'module-script'
    const {sourceLocation} = script; // 'https://example.com/app.js'
  }
});

If you see 'classic-script' or 'module-script' invoker types alongside high input delays, heavy script evaluation is likely blocking the thread. Breaking up bundles, deferring unused code, and auditing for removable code can all reduce this blocking time.

After Page Load

Input delays after load typically come from periodic work like setInterval callbacks, or earlier event callbacks that are still queued and running:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.toSorted((a, b) => b.duration - a.duration)[0];

  if (script) {
    const {invokerType} = script;        // 'user-callback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

The specific invoker types here help distinguish between blocking tasks:

  • 'user-callback': blocked by a setInterval, setTimeout, or requestAnimationFrame callback.
  • 'event-listener': blocked by an earlier input that was queued and still processing.
  • 'resolve-promise' or 'reject-promise': blocked by asynchronous work from earlier that resolved or rejected at the moment of interaction.

For these causes, script attribution data will show whether the delay stems from your own code or a third-party script.

High Presentation Delays

Presentation delay is the final stretch—after handlers finish and before the next frame is painted—when visual state changes trigger rendering work. The library exposes its duration per interaction:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691
});

Two common culprits account for most high presentation delays.

Expensive Style and Layout Work

Style recalculation and layout can be costly due to complex CSS selectors or large DOM sizes. LoAF data can surface the start of this work:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.toSorted((a, b) => b.duration - a.duration)[0];

  // Get necessary timings:
  const {startTime} = loaf; // 2120.5
  const {duration} = loaf;  // 1002

  // Figure out the ending timestamp of the frame (approximate):
  const endTime = startTime + duration; // 3122.5

  // Get the start timestamp of the frame's style/layout work:
  const {styleAndLayoutStart} = loaf; // 3011.17692309

  // Calculate the total style/layout duration:
  const styleLayoutDuration = endTime - styleAndLayoutStart; // 111.32307691

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running style and layout operation:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

LoAF doesn't report style/layout duration directly, but it does identify when that work started. To arrive at an accurate duration, subtract the style/layout start time from the frame's end time.

Long requestAnimationFrame Callbacks

requestAnimationFrame callbacks run after handlers but before style and layout work. If complex work makes them slow, presentation delay rises. Using the library's LoAF data can help identify such scenarios:

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 543.1999999880791

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.toSorted((a, b) => b.duration - a.duration)[0];

  // Get the render start time and when style and layout began:
  const {renderStart} = loaf;         // 2489
  const {styleAndLayoutStart} = loaf; // 2989.5999999940395

  // Calculate the `requestAnimationFrame` callback's duration:
  const rafDuration = styleAndLayoutStart - renderStart; // 500.59999999403954

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running requestAnimationFrame callback:
    const {invokerType} = script;        // 'user-callback'
    const {invoker} = script;            // 'FrameRequestCallback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

If a significant portion of the presentation delay occurs inside a requestAnimationFrame callback, make sure that work does something that results in a user-visible interface update. Work in these callbacks that only touches the DOM or styles will unneccessarily delay the next paint.

Field-collected data remains the most reliable basis for judging which interactions hurt real users most. Using tools like the web-vitals library—or a RUM provider—you can go from broad interaction metrics to specific, actionable fixes. Once problematic interactions are identified in field data, the next step is to move into the lab to reproduce and fix them.