What Is INP in Core Web Vitals? (And How to Improve It)
INP (Interaction to Next Paint) measures how responsive your page feels. Learn what causes poor INP, how to diagnose it, and the fixes that actually work.
INP (Interaction to Next Paint) replaced FID as the responsiveness metric in Core Web Vitals in March 2024. If your site felt fast but now shows poor INP scores, you’re not alone—INP is a harder bar to pass.
This guide explains what INP measures, why it matters, and how to fix it.
What Is INP?
INP (Interaction to Next Paint) measures how long it takes for your page to respond to user interactions—clicks, taps, and key presses.
Specifically, INP tracks:
- The time from when a user interacts
- To when the browser can paint the visual response
Unlike the old FID metric (which only measured the first interaction), INP considers all interactions throughout the page session and reports the worst one (at the 98th percentile).
INP Thresholds
| Rating | INP Score |
|---|---|
| Good | ≤200ms |
| Needs Improvement | 200ms–500ms |
| Poor | >500ms |
To pass Core Web Vitals, 75% of your page views need an INP of 200ms or less.
Why INP Matters
It’s a Google Ranking Factor
INP is part of Core Web Vitals, which are confirmed ranking signals. Pages with poor INP may be disadvantaged in search results, especially when competing against similar pages with better scores.
It Reflects Real User Experience
A page might load fast (good LCP) but feel sluggish when you try to interact with it. INP captures that “the page isn’t responding” frustration that makes users bounce.
Common symptoms of poor INP:
- Clicking a button and nothing happens for a moment
- Typing in a form field and characters appear delayed
- Tapping a menu and waiting for it to open
- Scrolling feels janky or stuttery
How to Measure INP
Field Data (What Google Uses)
PageSpeed Insights:
- Go to PageSpeed Insights
- Enter your URL
- Look at the “Field Data” section
- Check the INP metric
Search Console:
- Go to Core Web Vitals report
- Check which URLs have INP issues
- Note if it’s mobile, desktop, or both
Lab Data (For Debugging)
Chrome DevTools:
- Open DevTools (F12)
- Go to Performance tab
- Check “Web Vitals” checkbox
- Record while interacting with the page
- Look for “Interaction to Next Paint” in the timeline
Lighthouse:
- Lighthouse now includes TBT (Total Blocking Time) which correlates with INP
- Lower TBT generally means better INP
What Causes Poor INP?
INP problems almost always come down to JavaScript blocking the main thread. The browser can only do one thing at a time on the main thread, and if JavaScript is running, it can’t respond to user input.
Cause 1: Long Tasks
A “long task” is any JavaScript execution that takes more than 50ms. During that time, the browser can’t respond to clicks or key presses.
Common sources:
- Large JavaScript frameworks initializing
- Complex DOM manipulations
- Heavy calculations
- Synchronous API calls
Cause 2: Third-Party Scripts
Third-party scripts are the #1 cause of INP problems:
- Analytics (especially multiple trackers)
- Chat widgets
- Ad scripts
- Social media embeds
- A/B testing tools
- Personalization engines
These scripts often run heavy operations and don’t prioritize your page’s responsiveness.
Cause 3: Event Handler Complexity
When a user clicks a button, the event handler runs on the main thread. If that handler does too much work, INP suffers.
// Bad: Does too much in the click handler
button.addEventListener('click', () => {
// This blocks the main thread
const result = heavyCalculation();
updateUI(result);
sendAnalytics();
updateLocalStorage();
// User sees no response until ALL of this finishes
});
Cause 4: Layout Thrashing
Reading and writing to the DOM in rapid succession forces the browser to recalculate layout repeatedly:
// Bad: Layout thrashing
elements.forEach(el => {
const height = el.offsetHeight; // Read (forces layout)
el.style.height = height + 10 + 'px'; // Write (invalidates layout)
// Next iteration reads again, forcing another layout
});
How to Fix Poor INP
Fix 1: Break Up Long Tasks
Split heavy operations into smaller chunks that yield to the main thread:
// Before: One long blocking task
function processItems(items) {
items.forEach(item => heavyProcessing(item));
}
// After: Yielding to main thread
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
heavyProcessing(items[i]);
// Yield every 5 items
if (i % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
Modern approach using scheduler.yield() (when available):
async function processItems(items) {
for (const item of items) {
heavyProcessing(item);
// Yield to browser
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
}
}
}
Fix 2: Defer Non-Critical JavaScript
Move non-essential scripts out of the critical path:
<!-- Before: Blocks rendering -->
<script src="analytics.js"></script>
<!-- After: Loads after page is interactive -->
<script src="analytics.js" defer></script>
<!-- Even better: Load on user interaction -->
<script>
document.addEventListener('mouseover', () => {
const script = document.createElement('script');
script.src = 'analytics.js';
document.body.appendChild(script);
}, { once: true });
</script>
Fix 3: Audit and Reduce Third-Party Scripts
For each third-party script, ask:
- Is it essential? Remove anything you don’t actively use
- Can it load later? Delay loading until after page is interactive
- Is there a lighter alternative? Some tools have “lite” versions
- Can it run in a web worker? Move processing off the main thread
Quick wins:
- Remove duplicate analytics (do you really need GA, GTM, and Plausible?)
- Replace heavy chat widgets with lighter alternatives
- Load social embeds on demand (click to load)
- Use a tag manager to control when scripts load
Fix 4: Optimize Event Handlers
Keep event handlers lean—do the minimum needed to show visual feedback:
// Good: Immediate visual feedback, defer heavy work
button.addEventListener('click', async () => {
// 1. Show immediate feedback
button.classList.add('loading');
button.disabled = true;
// 2. Yield to let the browser paint
await new Promise(r => requestAnimationFrame(r));
// 3. Now do the heavy work
const result = await heavyCalculation();
updateUI(result);
// 4. Clean up
button.classList.remove('loading');
button.disabled = false;
});
Fix 5: Use Web Workers for Heavy Computation
Move CPU-intensive work off the main thread entirely:
// main.js
const worker = new Worker('heavy-work.js');
button.addEventListener('click', () => {
// Show loading state immediately
showLoading();
// Offload heavy work
worker.postMessage({ data: largeDataset });
});
worker.onmessage = (e) => {
// Update UI with results
updateUI(e.data.result);
hideLoading();
};
// heavy-work.js (Web Worker)
self.onmessage = (e) => {
const result = heavyCalculation(e.data);
self.postMessage({ result });
};
Fix 6: Avoid Layout Thrashing
Batch your DOM reads and writes:
// Bad: Read-write-read-write
elements.forEach(el => {
const h = el.offsetHeight;
el.style.height = h + 10 + 'px';
});
// Good: Read all, then write all
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});
Or use requestAnimationFrame to batch writes:
function updateElement(el, newHeight) {
requestAnimationFrame(() => {
el.style.height = newHeight + 'px';
});
}
INP Debugging Workflow
- Identify the problem page in Search Console or PageSpeed Insights
- Open Chrome DevTools → Performance tab
- Enable “Web Vitals” checkbox
- Record while interacting with the page (click buttons, type in forms)
- Look for long tasks (red triangles) near interactions
- Expand the task to see what’s running
- Identify the script causing the blockage
- Apply the appropriate fix from above
Common Framework-Specific Issues
React
- Large component re-renders blocking the main thread
- Fix: Use
React.memo,useMemo,useCallbackto reduce renders - Consider
useTransitionfor non-urgent updates
Next.js
- Hydration can cause long tasks on initial load
- Fix: Use dynamic imports, Suspense boundaries
- Consider
next/dynamicwithssr: falsefor heavy components
Vue
- Reactivity overhead on large datasets
- Fix: Use
v-oncefor static content,shallowReffor large objects
WordPress
- Plugin JavaScript bloat
- Fix: Use Asset CleanUp or Perfmatters to disable scripts per page
- See our WordPress Core Web Vitals guide
Frequently Asked Questions
Is INP replacing FID?
Yes. INP officially replaced FID as the responsiveness metric in Core Web Vitals in March 2024. INP is more comprehensive because it measures all interactions, not just the first one.
Does INP affect SEO?
Yes, INP is part of Core Web Vitals, which are Google ranking factors. Poor INP can disadvantage your pages in search results, especially when competing against pages with better scores.
Why is my INP score different in lab vs field data?
Lab tests (Lighthouse) measure TBT, which correlates with INP but isn’t the same metric. Field data (real users) shows actual INP from Chrome users visiting your site. They can differ because:
- Real users have different devices and network conditions
- Real users interact differently than lab simulations
- Third-party scripts may behave differently with real traffic
My site has good FID but poor INP. Why?
FID only measured the first interaction. INP measures all interactions and reports the worst one. Your page might respond quickly to the first click but slow down on subsequent interactions (common with JavaScript-heavy sites).
Next Steps
Run an audit to identify your specific INP issues:
Tags:
Share this article:
Ready to improve your SEO?
Get a free SEO audit and see exactly what needs fixing on your site
Start Free Audit