Skip to content

About

Interaction attribution for INP: joins Event Timing and Long Animation Frames to the React fiber tree.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

react-inp-blame: interaction attribution for INP, with a 400 ms click split into its handler

npm version CI status MIT license Node engines

The demo's sign-in page: an email and a password typed in, a click on Log in, the badge showing the page's INP, the panel opening, and the login row expanding into the explanation

Click Log in in the demo. The report shows a 400 ms interaction, about 399 ms in the handler, and a React render that landed after the screen updated. INP didn't count that later render. Without the timeline, it is easy to investigate the render and miss the handler.

react-inp-blame joins browser interaction timing to the React tree and shows where the time went, with a component or handler name when it has one. The name can be wrong, check it against the profiler. 1.0.0 makes the API and report shape stable, with documented exceptions. That is an API promise, not an accuracy claim. Read the validation status and limits.

Try the demo. Every scenario there is slow on purpose. Click something and read what the badge blames.

30-second quickstart

In an existing Next.js or Vite React app:

npm install react-inp-blame

Use the setup below for your framework, restart its dev server, then click a slow control and open the badge. Both setups are development-only by default. Production needs an explicit opt-in.

Start with Next.js 14.2 or later

// next.config.ts
import type { NextConfig } from "next";
import { withInpBlame } from "react-inp-blame/next";

const nextConfig: NextConfig = { /* config options here */
};

// Your config first, this library's options second. They are not Next.js config keys.
export default withInpBlame(nextConfig, { runtime: { overlay: true } });

On Next.js 15.3 to 16.2, add one line to instrumentation-client.ts as well. withInpBlame prints it until the file has it (14.2 to 15.2 need nothing more):

// instrumentation-client.ts
export { onRouterTransitionStart } from "react-inp-blame/next-client";

Start with Vite

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { inpBlame } from "react-inp-blame/vite";

export default defineConfig({ plugins: [react(), inpBlame({ runtime: { overlay: true } })],
});

React Router in framework mode, Remix, TanStack Start and Astro write their own HTML, which this plugin never sees on its own, so each has a setup of its own: React Router, Remix, TanStack Start, Astro.

What you will see. Reload the page. A small dark badge sits in the corner and reads INP — until you interact. Click something slow and it shows the page's INP so far in milliseconds: green at 200 or under, amber up to 500, red above. Click the badge for a panel of the recent slow interactions, and click a row for the whole explanation. From the demo's sign-in page, in development, this is report.verdict:

400 ms click on button "Log in" in SignInPage. The click handler handleLogin ran for
about 399 ms; React's own render took under 1 ms. A second React render landed 295 ms
after the screen updated: 81 ms rendering 260 components from SignInDemo down, 241 of them
inside ProfilePage, mostly PhotoTile (240 of the 260, 72 ms). INP doesn't count it, and
what started it can't be told.

The overlay option, development numbers and reading reports in code are in Using the badge. Next.js 14.2 setup goes in next.config.mjs, shown there too.

Send attribution with INP

The web-vitals attribution build owns the page's INP measurement. This library adds React context to it. For component names on the interaction target, generateTarget needs no runtime installation:

import { onINP } from "web-vitals/attribution";
import { generateTarget } from "react-inp-blame/web-vitals";

onINP((metric) => console.log(metric), { generateTarget });

Use web-vitals 5.1 or later. The integration guide covers the whole-report path (attributeINP), production component names and the cases where no report is available. Event Timing supplies the interaction's duration and phases, and Long Animation Frames adds script and forced-layout evidence on Chromium 123 and later. For an existing OpenTelemetry web-vitals pipeline, see the processor recipe.

How reliable is it

The current five-app cohort has no published accuracy or overhead results. Its build and interaction integrations are still pending, so selecting those apps is not evidence that attribution has been validated on them. See the cohort status and measurement plan.

The benchmark harness now selects five everyday apps. Their build and interaction integrations are pending, and no accuracy or overhead results exist for this cohort. The measurement plan describes the protocol and selection amendment. See validation status before treating attribution as evidence. Local copy and test steps: reproduction. What 1.0 means: What 1.0 means.

A report joins timing evidence with React activity. A component name is a lead to investigate, not proof that the component caused the delay. Coinciding work may be unrelated, production builds lack React render times, and browser support limits the evidence available. Check a verdict against a browser performance trace and the known limits before acting on it.

Side-by-side with Sentry, react-scan, web-vitals attribution and React 19.2 Performance tracks: Compared with other tools.

When the blame is wrong

If a report names the wrong component or handler, or a slow interaction gets no report at all, please open an issue with the wrong or missing blame form. What to paste, and what the form asks for: Reporting a wrong blame.

Browser support

Chromium Firefox Safari
Event Timing interactionId (required) 96 144 26.2
Long Animation Frames: scripts and forced layout 123 no no
Performance panel tracks 128 no no

Notes on timer precision and missing APIs: browser support notes.

Versions

CI runs Outside that
react-dom 17.0.2, 18.2.0, 18.3.1, 19.0.0, 19.1.9, 19.2.8, 19.3.0, and the canary next@canary brings Outside 17 to 19 that root is not read, and one warning says so
Next.js 14.2.35, 15.3.9, 15.5.26, 16.2.12, 16.3.5 and canary Below 14.2 the wrapper leaves the config as it was and says why
Vite 6.4, 7.3 and 8.3; a build on Vite 5 Not tried below 5
React Router, Remix, TanStack Start React Router 7.18 and 8.4, Remix 2.17 on Vite, TanStack Start 1.168 Not tried
Astro 7.3 Not tried
webpack 5.111 Not tried
Node, for the build plugins Installs and loads on 20.19, tests on 22 and 24 engines asks for 20.19

Peer dependency notes, Jest setup, the 1.x promise and the support window are in Versions.

What it costs

It is a development tool first, and running it in production is optional: this is what it costs where it runs. On two of the demo's scenarios, a click that re-renders 801 components and a keystroke that re-renders 1441, the library's own time was 1.3 to 3.4 ms per interaction at the median and 3.8 ms at worst at p95, in production and development builds (how that was measured). With enabled at its default, neither plugin adds anything to a production build (on Next.js 15.3 to 16.2 the line's module stays, as an empty function); where it loads:

Bundle (rolldown 1.2.8, minified ESM, gzip at zlib's default level) Minified Gzip
react-inp-blame/auto: everything that loads with the page 101.3 KB 35.8 KB
The badge and panel, a chunk loaded by import() only when shown 18.1 KB 7.1 KB
Of /auto, what has to run before react-dom: the hook, the fiber reading, the observers 32.3 KB 11.7 KB
react-inp-blame/web-vitals, on top of /auto 1.6 KB 0.9 KB
react-inp-blame/otel, on top of /auto 3.5 KB 1.6 KB

How the bundle table is kept current: What it costs.

Labels and personal data

Reports are made to be forwarded to error trackers and analytics, so target.label never reads a form field's value. The details are under labels and personal data in the API page, and what the library reads, changes and keeps on a page is in SECURITY.md. More: Labels and personal data.

Documentation

The docs have the rest.

Troubleshooting

Each warning the library prints ends with a link into this README. Most land on their line below, which opens the answer in docs/troubleshooting.md; the Vite plugin's setup advice lands on its setup under Documentation. To see whether the library installed at all, and why not, add debugGlobal: true and read __REACT_INP_BLAME__.stats(). If your problem is not listed, open a setup problem issue with the warning text.

In the browser: install() ran after a React root had already rendered · React has rendered, but no react-dom has registered · Most component names are one or two characters · Long Animation Frames on this page list no scripts · A copy from an incompatible version is already on this page · This browser has no Event Timing interactionId · install() had already run · The page's DevTools hook turns React's developer tools support off · hook: 'shim' found a React DevTools hook already installed · The DevTools hook was replaced after React registered · react-dom is outside React 17 to 19 · The fiber tree is not the shape this library reads · Reading a commit threw · An error inside the library was caught · The badge and panel could not be shown · The badge and panel could not be drawn

At build time, any setup: Left out of this production build

At build time, Next.js: Next.js is older than 14.2 · Next.js before 15.3 under Turbopack · Add this line to instrumentation-client.ts · Your config already has a Turbopack rule for the same files

At build time, Vite: The install script never reaches a page · The install chunk imports a chunk that runs react-dom · entry needs a manualChunks function · The bundler gives manualChunks no getModuleInfo


Docs · Changelog · Contributing · Security · MIT license

About

Interaction attribution for INP: joins Event Timing and Long Animation Frames to the React fiber tree.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages