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.
In an existing Next.js or Vite React app:
npm install react-inp-blameUse 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.
// 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";// 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.
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.
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.
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.
| 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.
| 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.
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.
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.
The docs have the rest.
- Install, every setup in full: Next.js, Vite, React Router, Remix, TanStack Start, Astro, webpack or Rspack, and a Vite build with no HTML page (Laravel, Rails, Django).
- With web-vitals: the React side added to web-vitals' INP attribution, sent to Sentry or to GA4.
- With OpenTelemetry, experimental: the blame added to the INP event OpenTelemetry, Honeycomb, Elastic, Embrace or Grafana Faro already sends.
- API:
onInteraction,install(options), every field of a report and the badge and panel. - How it works: clicks before hydration, the INP estimate, what it reads from React and its own time per interaction.
- Known limits: what it can't see, and where it has to guess. Worth reading before you trust a verdict on a big app.
- Benchmark plan and pending integrations for the five-app cohort, and the design notes.
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
