So here's an experience that used to not matter at all. A user taps your "Add to Cart" button and nothing happens for 400 milliseconds. In human time, that's barely noticeable right? Except it's not. People can perceive delays as short as 100ms, and after 300ms things start feeling broken. After 500ms people are already wondering if they should tap again.
INP (Interaction to Next Paint) measures exactly this: the time between when a user does something (click, tap, keypress) and when the browser actually shows the visual response. Google made it a Core Web Vital in March 2024, replacing First Input Delay. And a lot of sites that were "passing" suddenly weren't.
What INP Measures (And Why It's Harder Than FID)
FID was easy to pass. It only measured the delay before the browser started running your event handler, and only for the very first interaction. So if your page loaded clean and the first click was fast, you passed. Even if every subsequent click was sluggish.
INP is different. It measures all interactions throughout the entire page session and reports the worst one (technically the 98th percentile for pages with many interactions). And it measures the full round-trip: input delay + processing time + presentation delay.
INP Thresholds
Good: Under 200 milliseconds
Needs Improvement: 200 to 500 milliseconds
Poor: Over 500 milliseconds
Google measures this using field data from the Chrome User Experience Report (CrUX) at the 75th percentile. Lab tools like Lighthouse can measure TBT (Total Blocking Time) as a proxy, but actual INP can only be measured with real user interactions.
The Three Parts of an Interaction
Every interaction has three phases, and any of them can be the bottleneck:
1. Input Delay: Time between the user's click/tap and when the browser starts running your event handler. This is slow when other JavaScript is already running on the main thread (a "long task" blocking things).
2. Processing Time: How long your actual event handler takes to run. If your click handler does 200ms of computation, that's 200ms the user is waiting.
3. Presentation Delay: Time between when your event handler finishes and when the browser renders the visual update. This is slow when the DOM changes triggered by your handler cause expensive style recalculations or layout operations.
Fix #1: Break Up Long Tasks
The single biggest cause of bad INP is long JavaScript tasks hogging the main thread. A "long task" is anything over 50ms. While it's running, the browser literally can't respond to clicks. The user's tap just sits in a queue waiting.
The fix: break big operations into smaller chunks using scheduler.yield() or
setTimeout:
// Bad: blocks main thread for 300ms
function processAllItems(items) {
items.forEach(item => {
heavyComputation(item); // 3ms each x 100 items = 300ms blocked
});
}
// Good: yields to browser between chunks
async function processAllItems(items) {
const CHUNK_SIZE = 10;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => heavyComputation(item));
// Give the browser a chance to handle clicks
await new Promise(resolve => setTimeout(resolve, 0));
}
}
That setTimeout(resolve, 0) isn't really zero. It gives the browser a small window to check if
anything else needs attention (like a user clicking something). The newer scheduler.yield() API
does this more elegantly, but it's not in all browsers yet.
Fix #2: Defer Non-Critical JavaScript
Every script that loads and executes on page load is a potential INP killer. Analytics scripts, chat widgets, social share buttons, third-party ad scripts. Each one might run a long task that blocks the main thread right when a user decides to click something.
The Third-Party Script Problem
A typical e-commerce site loads 15-25 third-party scripts. Google Analytics, Facebook Pixel, Hotjar, Intercom, review widgets, recommendation engines. Each one is fighting for main thread time. Individually they're maybe 30ms each. Together they're 500ms of blocked main thread time.
Fixes that actually work:
- Add
deferto every script that can wait (analytics, tracking, chat widgets) - Delay non-essential scripts until after user interaction: only load the chat widget when the user scrolls or after 5 seconds
- Remove scripts you don't use. That Pinterest button you added 3 years ago? If nobody clicks it, remove it. Sounds obvious but I see it constantly.
- Self-host critical third-party scripts to avoid DNS lookup and connection overhead
For a detailed guide on this, check out our JavaScript deferring guide.
Fix #3: Make Event Handlers Faster
If your click handler does too much work, INP will be slow even if the input delay is zero. Here are the biggest offenders:
Don't do DOM reads and writes together
Reading layout properties (like offsetHeight) and then writing to the DOM causes "forced
synchronous layout." The browser has to recalculate everything between each read and write:
// Bad: forces layout thrashing
button.addEventListener('click', () => {
const height = element.offsetHeight; // READ
element.style.height = height + 10 + 'px'; // WRITE
const width = element.offsetWidth; // READ (forces layout again!)
element.style.width = width + 10 + 'px'; // WRITE
});
// Good: batch reads, then batch writes
button.addEventListener('click', () => {
const height = element.offsetHeight;
const width = element.offsetWidth;
// Both reads done, now write
element.style.height = height + 10 + 'px';
element.style.width = width + 10 + 'px';
});
Use requestAnimationFrame for visual updates
If your event handler needs to update the UI, wrap the visual changes in requestAnimationFrame
so they happen at the right time in the rendering cycle:
button.addEventListener('click', () => {
// Do your logic immediately
const newData = calculateSomething();
// Schedule visual update for next frame
requestAnimationFrame(() => {
updateUI(newData);
});
});
Fix #4: Reduce DOM Size
A large DOM (over 1,500 nodes) makes every style recalculation expensive. When your event handler changes a class name or modifies a style, the browser has to figure out which of those 1,500+ nodes are affected. More nodes = more work = slower INP.
Practical ways to shrink your DOM:
- Remove hidden elements instead of just setting
display: none(they still exist in the DOM) - Use virtual scrolling for long lists (only render what's visible)
- Simplify your HTML structure (do you really need 5 nested wrapper divs?)
- Lazy-render below-the-fold content using IntersectionObserver
Fix #5: Move Heavy Work Off the Main Thread
For genuinely heavy computation (image processing, data manipulation, complex calculations), move the work to a Web Worker. Web Workers run on a separate thread so they never block the main thread:
// main.js
const worker = new Worker('heavy-work.js');
button.addEventListener('click', () => {
// Show immediate visual feedback
button.textContent = 'Processing...';
// Send heavy work to worker
worker.postMessage({ data: bigDataSet });
});
worker.addEventListener('message', (e) => {
// Update UI with result
displayResults(e.data);
button.textContent = 'Done!';
});
// heavy-work.js (separate file, separate thread)
self.addEventListener('message', (e) => {
const result = expensiveCalculation(e.data);
self.postMessage(result);
});
The user clicks, sees immediate feedback ("Processing..."), and the heavy work happens in the background without freezing anything.
How to Debug INP
Chrome DevTools Performance Panel
- Open DevTools > Performance tab
- Hit Record
- Click, tap, and type on your page like a real user would
- Stop recording and look for long tasks (yellow bars > 50ms)
- Expand the call stack to see exactly which functions are slow
The Web Vitals Extension
Install the Web Vitals Chrome extension, enable "Console logging," then interact with your page. It'll log every interaction with its INP breakdown (input delay, processing, presentation) right in the console.
The INP Fix Checklist
Run Through This List
1. No long tasks over 50ms during user interaction
2. All non-critical scripts are deferred or delayed
3. Event handlers don't do layout thrashing
4. Visual updates use requestAnimationFrame
5. Heavy computation is in Web Workers
6. DOM size is under 1,500 nodes
7. Third-party scripts are audited and unnecessary ones removed
8. Click handlers give immediate visual feedback
FAQ
What's a good INP score?
Under 200ms. This is measured from real user interactions, not lab tests. Lighthouse doesn't directly measure INP, it uses Total Blocking Time (TBT) as a rough proxy.
My FID was fine but INP fails. Why?
FID only measured the first interaction's input delay. INP measures ALL interactions for their full duration (input delay + processing + presentation). FID let you pass if your page loaded clean. INP catches sluggish interactions that happen later, like clicking a dropdown or opening a modal.
Do third-party scripts affect INP?
Absolutely. Third-party scripts are one of the top causes of INP failures. Each script runs on the main thread and can create long tasks that block user interactions. The most common culprits: analytics scripts, chat widgets, ad networks, and customer service tools. Defer or delay-load everything that isn't critical for the first render.
Does INP matter for SEO?
Yes. INP is one of Google's three Core Web Vitals and a ranking signal. Sites with INP over 500ms can be ranked lower than competitors with good INP scores. It's not the biggest ranking factor, but it's a tiebreaker for sites that are otherwise equal in content quality.
See Your INP Score
VitalsFixer shows your Total Blocking Time (the closest lab proxy for INP) and identifies which scripts are creating long tasks on your page.
Analyze My Site Free →JavaScript giving you a headache?
Our engineers profile your scripts, break up long tasks, and defer everything that's slowing your interactions. Real humans, 48-hour turnaround, money-back guarantee.
View Expert Fix Service →