INP Optimization Guide 2025: Fix Interaction to Next Paint
Complete guide to optimizing Interaction to Next Paint (INP), Google's newest Core Web Vital. Learn why your site fails INP and get code fixes that work.
Written by Chris Drinkard
14 years in hospitality management
5 years helping independent hotels grow their online presence. Passionate about AI, search, and digital strategy.
⚠️ Critical Update: INP (Interaction to Next Paint) officially replaced FID as a Core Web Vital in March 2024. If your site passed Core Web Vitals before but fails now, INP is likely the culprit. This guide shows you exactly how to diagnose and fix INP issues.
Related (Quick Wins)
What Is INP (Interaction to Next Paint)?
Interaction to Next Paint (INP) measures how quickly your page responds to user interactions. It tracks the time from when a user clicks, taps, or presses a key until the browser paints the visual response.
INP Measures Three Phases:
- Input Delay: Time from user interaction until event handlers start running
- Processing Time: Time spent executing event handler callbacks
- Presentation Delay: Time from event handlers completing until browser paints the update
INP = Input Delay + Processing Time + Presentation Delay
Good
≤ 200ms
Needs Improvement
200-500ms
Poor
> 500ms
Unlike other metrics that measure a single moment, INP tracks every interaction throughout a page visit. It then reports the worst interaction (at the 98th percentile for pages with many interactions, or the single worst for pages with few).
INP vs FID: Why Google Made the Change
FID (First Input Delay) only measured the first interaction and only the input delay phase. This had major blindspots:
| Aspect | FID (Old) | INP (New) |
|---|---|---|
| Interactions Measured | First only | All interactions |
| What's Measured | Input delay only | Full interaction lifecycle |
| Good Threshold | ≤ 100ms | ≤ 200ms |
| Typical Problem | Startup JavaScript | Heavy event handlers, re-renders |
| Framework Impact | Moderate | Significant |
Why Many Sites Now Fail Core Web Vitals
A site could have perfect FID (first click responds fast) but terrible INP (later interactions are slow). This is common with SPAs, heavy React/Vue apps, and sites with complex interactive features. When Google switched to INP in March 2024, many "passing" sites suddenly failed.
How to Measure Your INP Score
Lab Tools (Simulated)
- Chrome DevTools: Performance panel → Check "Interactions" in timeline
- Lighthouse: Shows estimated INP in Performance audit (but limited to interactions you trigger)
- PageSpeed Insights: Shows both lab simulation and field data
Field Tools (Real Users)
- Google Search Console: Core Web Vitals report → INP status
- PageSpeed Insights: "Field Data" section (if available)
- Chrome UX Report (CrUX): Public dataset of real user metrics
- web-vitals library: Add to your site for custom RUM
// Add INP monitoring with web-vitals library
import {onINP} from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value, 'ms');
console.log('Interaction:', metric.entries[0]?.name);
// Send to your analytics
sendToAnalytics({
name: 'INP',
value: metric.value,
rating: metric.rating, // 'good', 'needs-improvement', 'poor'
interaction: metric.entries[0]?.name
});
}); 💡 Pro Tip: Use the Web Vitals Extension
Install the Web Vitals Chrome extension to see INP in real-time as you interact with any page. Click around your site to find which interactions cause high INP.
Diagnosing INP Problems
Before fixing INP, you need to identify which interactions are slow and why. Here's a systematic approach:
Step 1: Find Slow Interactions
- Open Chrome DevTools → Performance tab
- Enable "Interactions" track (click gear icon)
- Record while interacting with your page
- Look for red/yellow interaction bars (slow)
- Click an interaction to see breakdown
Step 2: Identify the Slow Phase
Each interaction has three phases. Identify which is longest:
Input Delay
Main thread was busy when user clicked
Fix: Break up long tasks, defer non-critical JS
Processing Time
Event handler took too long
Fix: Optimize handler code, reduce work
Presentation Delay
Rendering/painting took too long
Fix: Simplify DOM updates, reduce layout thrashing
Common Culprits
- Accordion/Tab clicks: Re-rendering large content sections
- Form submissions: Validation + API calls blocking
- Navigation menu: Complex animations or layout changes
- Infinite scroll: Loading + rendering new items
- Filter/Sort: Re-rendering large lists
- Modal opens: Lazy-loaded content + overlay rendering
Fix #1: Break Up Long JavaScript Tasks
Any JavaScript task over 50ms is a "Long Task" that blocks the main thread. During a long task, the browser can't respond to user input, causing input delay.
Use scheduler.yield() (Modern Approach)
// Before: One long task
function processItems(items) {
for (const item of items) {
doExpensiveWork(item); // Blocks for entire loop
}
}
// After: Yield to browser between chunks
async function processItems(items) {
for (const item of items) {
doExpensiveWork(item);
// Let browser handle pending interactions
if ('scheduler' in window) {
await scheduler.yield();
}
}
} Use setTimeout for Older Browsers
// Polyfill for scheduler.yield()
function yieldToMain() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function processItems(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 => doExpensiveWork(item));
await yieldToMain(); // Let browser breathe
}
} Use requestIdleCallback for Non-Urgent Work
// Defer analytics, tracking, prefetching
requestIdleCallback(() => {
sendAnalytics();
prefetchNextPage();
}, { timeout: 2000 }); // Max 2s wait Fix #2: Optimize Event Handlers
Event handlers run during the "processing time" phase. Long handlers = high INP.
Debounce Rapid-Fire Events
// Before: Handler runs on every keystroke
input.addEventListener('input', (e) => {
searchAPI(e.target.value); // Too frequent!
});
// After: Debounce to reduce calls
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
input.addEventListener('input', debounce((e) => {
searchAPI(e.target.value);
}, 300)); Separate Visual Updates from Data Processing
// Before: Everything in one handler
button.addEventListener('click', async () => {
showLoadingSpinner();
const data = await fetchData(); // Blocking!
updateUI(data);
sendAnalytics();
});
// After: Immediate visual feedback, defer the rest
button.addEventListener('click', () => {
// Immediate: Visual feedback (fast INP)
showLoadingSpinner();
// Deferred: Heavy work
requestAnimationFrame(async () => {
const data = await fetchData();
updateUI(data);
requestIdleCallback(() => {
sendAnalytics();
});
});
}); Avoid Synchronous Storage Access
// Bad: Synchronous localStorage in handler
button.addEventListener('click', () => {
const prefs = JSON.parse(localStorage.getItem('prefs')); // Blocks!
applyPreferences(prefs);
});
// Good: Read storage outside handlers, cache in memory
let cachedPrefs = null;
// Load once on page start
requestIdleCallback(() => {
cachedPrefs = JSON.parse(localStorage.getItem('prefs') || '{}');
});
button.addEventListener('click', () => {
applyPreferences(cachedPrefs); // Fast - already in memory
}); Fix #3: Tame Third-Party Scripts
Third-party scripts (analytics, ads, chat widgets) often run long tasks that block interactions. They're the #1 cause of poor INP on otherwise well-optimized sites.
Audit Third-Party Impact
- Open DevTools → Performance tab → Record page load
- Look for long tasks (red bars) in the flame chart
- Expand tasks to see which scripts caused them
- Third-party domains are usually obvious (googletagmanager.com, etc.)
Load Third-Party Scripts After Interaction
// Load chat widget only after user scrolls or clicks
let chatLoaded = false;
function loadChatWidget() {
if (chatLoaded) return;
chatLoaded = true;
const script = document.createElement('script');
script.src = 'https://chat-widget.com/widget.js';
document.body.appendChild(script);
}
// Trigger on interaction or scroll
window.addEventListener('scroll', loadChatWidget, { once: true });
document.addEventListener('click', loadChatWidget, { once: true });
// Or after a delay
setTimeout(loadChatWidget, 5000); Use loading="lazy" for Embeds
<!-- YouTube embeds: Use facade pattern --> <lite-youtube videoid="VIDEO_ID"></lite-youtube> <!-- Or native lazy loading for iframes --> <iframe src="https://www.youtube.com/embed/VIDEO_ID" loading="lazy" title="Video title" ></iframe>
Third-Party Scripts to Audit
- • Google Tag Manager (GTM) with many tags
- • Live chat widgets (Intercom, Drift, Zendesk)
- • Heatmap tools (Hotjar, FullStory, Clarity)
- • A/B testing (Optimizely, VWO)
- • Advertising scripts (Google Ads, Facebook Pixel)
- • Social widgets (Twitter embeds, Facebook Like buttons)
Fix #4: Reduce Rendering Work
The "presentation delay" phase measures how long it takes to paint visual updates. Large DOM trees and layout thrashing are common culprits.
Avoid Layout Thrashing
// Bad: Read-write-read-write forces multiple layouts
elements.forEach(el => {
const height = el.offsetHeight; // Read (forces layout)
el.style.height = height + 10 + 'px'; // Write
});
// Good: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // All writes
}); Use CSS content-visibility for Long Pages
/* Skip rendering off-screen sections */
.section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Estimated height */
} Virtualize Long Lists
For lists with 100+ items, render only visible items:
- React:
react-windoworreact-virtualized - Vue:
vue-virtual-scroller - Vanilla JS:
virtual-scrollerweb component
Fix #5: Framework-Specific Optimizations
React
// Use React 18's concurrent features
import { useTransition, useDeferredValue } from 'react';
function SearchResults({ query }) {
// Mark state updates as non-urgent
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
const handleSearch = (value) => {
// Urgent: Update input immediately
setQuery(value);
// Non-urgent: Defer results update
startTransition(() => {
setResults(search(value));
});
};
return (
<div className={isPending ? 'opacity-50' : ''}>
{results.map(r => <Result key={r.id} {...r} />)}
</div>
);
} Vue
<!-- Use v-memo for expensive lists -->
<template>
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.selected]"
>
<ExpensiveComponent :item="item" />
</div>
</template>
<!-- Use defineAsyncComponent for lazy loading -->
<script setup>
import { defineAsyncComponent } from 'vue';
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
</script> General Framework Tips
- Avoid re-renders on every keystroke: Debounce input handlers
- Memoize expensive computations: useMemo, computed properties
- Lazy load routes/components: Don't load what users won't see
- Use production builds: Dev builds have extra overhead
- Consider Islands Architecture: Astro, Fresh, Qwik partial hydration
Monitoring INP in Production
Lab testing can't catch all INP issues. Real users have different devices, networks, and interaction patterns. Set up Real User Monitoring (RUM):
// Complete INP monitoring setup
import {onINP} from 'web-vitals';
onINP((metric) => {
// Get interaction details
const entry = metric.entries[0];
const interactionTarget = entry?.target?.tagName;
const interactionType = entry?.name; // 'click', 'keydown', etc.
// Send to your analytics
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
metric: 'INP',
value: metric.value,
rating: metric.rating,
url: window.location.href,
interactionTarget,
interactionType,
// Include device info
deviceMemory: navigator.deviceMemory,
connection: navigator.connection?.effectiveType
}),
keepalive: true // Ensure it sends even on page exit
});
}); 🎯 Monitor with Rankture
Rankture's Core Web Vitals monitoring automatically tracks INP along with LCP and CLS. Get alerts when your INP regresses and see which pages need attention. Learn more about automated CWV monitoring →
Frequently Asked Questions
What is a good INP score?
What replaced FID in Core Web Vitals?
Why is my INP score so bad when FID was fine?
Does INP affect SEO rankings?
How do I measure INP on my site?
What causes bad INP scores?
How long does it take to see INP improvements in Search Console?
Can I pass Core Web Vitals with bad INP?
Related Core Web Vitals Guides
Check Your INP Score Now
See how your site performs on all Core Web Vitals including INP. Get actionable recommendations to pass Google's thresholds.
Run Free Audit →