Skip to content

How to fix high INP (over 200ms)

High INP — taps and clicks taking more than 200ms to respond — almost always comes from one cause: too much JavaScript running on the main thread. The fix is not a single change; it's a disciplined audit of what's blocking.

1. Find the slow interactions

Install the web-vitals library on your site and log INP events to your analytics:

import {onINP} from 'web-vitals';
onINP((metric) => {
  // send to analytics with metric.entries[0].target (the element that was interacted with)
});

After a week you'll know exactly which buttons / links / inputs are the slowest. You can't fix what you can't see.

2. Break up long tasks

Any task over 50ms on the main thread blocks input. In an event handler, wrap expensive work in scheduler.yield() (or the postTask polyfill) between chunks:

async function handleFilterChange() {
  applyFiltersQuickly();
  await scheduler.yield();
  renderFilteredListInBackground();
}

This breaks one 400ms task into two 200ms tasks with a yield in between — the browser can process input between them.

3. Audit third-party JavaScript

The single biggest INP killer is third-party scripts. Run Chrome DevTools → Performance → record a click → look at what's running.

Typical offenders:

  • Google Tag Manager with 20 tags loaded
  • Chat widgets (Intercom, Zendesk) doing polling work
  • A/B test SDKs running experiment logic on every click
  • Ad networks running auction logic

The fix: be ruthless. Every third-party script must justify its INP cost. Defer, lazy-load (load only after first interaction), or remove.

4. Reduce React re-renders

In React apps, a common INP pattern is: user types → state updates → entire tree re-renders → 600ms INP.

  • useMemo / useCallback where the profiler shows wasted renders
  • React.memo on expensive components
  • Split context so a change in A doesn't re-render B
  • Virtualize long lists with react-window or @tanstack/react-virtual
  • Move derived state to useMemo with a stable dependency array

5. Use content-visibility: auto for long pages

For pages with long stacks of sections (docs, feed pages, etc.), apply content-visibility: auto on below-the-fold sections. The browser skips layout and paint for off-screen content until needed.

6. Move heavy computation to a worker

If a click triggers real CPU work (image processing, markdown parsing, search indexing) — put it in a Web Worker. The main thread stays free for input.

7. Measure and iterate

INP is a p75 metric, so you're chasing the worst typical user. After every fix, re-deploy, wait a week, check p75. If p75 dropped, you're winning. If not, the bottleneck is elsewhere — go back to step 1 and find the new worst interaction.

A note on FID

If you still see FID-only in old articles and dashboards: FID was retired in March 2024. It's INP now. Dashboards that still show FID are stale.

By Paulo de Vries · Published

Frequently asked questions

Why is my INP over 200ms?
Too much JavaScript on the main thread. Audit third-party scripts (analytics, ads, chat), break up long event handlers, reduce unnecessary React re-renders, and move heavy work to Web Workers.
How do I measure real-user INP?
Install the web-vitals npm library and log onINP events to your analytics with the target element. After a week you will know exactly which interactions are slow.
What is scheduler.yield()?
A new browser API that lets long tasks yield to the main thread between chunks of work. Use it (or a setTimeout(0) fallback) to break 400ms tasks into two 200ms tasks.

Check a site's vitals

Explore by industry

See how real-world sites in each vertical perform on Core Web Vitals.

Related guides

← All guides