INP Optimization: Fix Interaction to Next Paint | SlapMyWeb
Performance10 min read
INP Optimization: Fix Interaction to Next Paint
INP optimization made practical: measure, debug, and fix slow Interaction to Next Paint with copy-paste JavaScript techniques that pass Core Web Vitals.
SlapMyWeb TeamΒ·
Interaction to Next Paint (INP) is the Core Web Vital that measures how quickly your page responds to user interactions β clicks, taps, and keypresses β by timing the gap between the interaction and the next frame the browser paints. A good INP is 200 milliseconds or less at the 75th percentile of real-user interactions. INP replaced First Input Delay (FID) as an official Core Web Vital in March 2024, and unlike FID it grades every interaction across the page lifecycle, not just the first one. This guide shows you how to measure it, find the slow interactions, and fix them with real JavaScript you can paste into your codebase today.
If a button takes a beat too long to react, users assume the page is frozen and bail. INP is the metric that catches exactly that lag β and Google now uses it as a ranking signal. Let's make your UI feel instant.
What Is INP (Interaction to Next Paint)?
Interaction to Next Paint measures your page's overall responsiveness to user input across the whole time someone spends on it. It tracks the time from when a user interacts (click, tap, or keypress) to when the browser renders the next frame showing visual feedback.
Each interaction's latency is the sum of three phases:
Input delay β time before the event handler can even start running (usually because the main thread is busy)
Processing time β how long your event handlers take to execute
Presentation delay β time to recalculate layout, paint, and composite the next frame
Unlike FID, which only measured the input delay of the first interaction, INP records all interactions and reports roughly the worst one (close to the 98th percentile of a page's interactions, surfaced at the 75th percentile across users in field data). That makes it a far harder bar to clear β one janky modal or one slow search box can wreck your score.
These thresholds are defined by Google on web.dev's INP documentation. The 200 ms target maps to the rhythm at which humans perceive a response as immediate.
A person tapping a button on a phone with the website responding instantly with a pressed-state highlight
Why INP Matters More Than You Think
INP is a confirmed part of Google's page experience signals, so it feeds directly into ranking β most decisively in competitive niches where other factors are close to equal. But the ranking story is secondary to the experience story:
Bounce and abandonment. When an interaction feels frozen, people don't wait politely. They hit back and try the next result.
Perceived quality. Laggy taps make an otherwise polished brand feel cheap and unreliable, no matter how good the content is.
Mobile reality. Budget Android phones have far weaker CPUs than the laptop you're testing on, so main-thread congestion hits mobile users much harder. Your desktop Lighthouse score is lying to you about how the page actually feels in someone's hand.
INP sits alongside LCP and CLS in the Core Web Vitals set, and it's the one that's hardest to fake β it reflects how well your JavaScript behaves under real-world load. If you run an online store, the stakes are higher still; see Core Web Vitals for e-commerce for revenue-specific fixes.
Run a free SlapMyWeb audit to see your INP score alongside every other Core Web Vital, with the exact interactions flagged.
1. Identify the Worst Offenders
Before optimizing anything, measure what is actually slow. Guessing wastes hours.
Chrome DevTools Performance tab (lab debugging):
Open DevTools (F12) β Performance tab.
Click Record, then interact with your page β click buttons, open menus, type in forms.
Stop recording and look for Long Tasks (red-flagged blocks) in the main-thread timeline.
The longest tasks that fire during your interactions are your INP culprits.
Field data (the truth): Check the Core Web Vitals report in Google Search Console and the CrUX data behind it. Lab tools like Lighthouse run on a simulated throttled CPU and often report better INP than real users on real devices experience. Field data is what Google ranks on, so trust it over lab numbers when they disagree. If you're not sure your pages are even being measured, confirm your site is indexed by Google first.
2. Break Up Long Tasks With scheduler.yield()
The single biggest cause of poor INP is long-running JavaScript that blocks the main thread. Any task over 50 ms is officially a "long task" and prevents the browser from responding to input while it runs.
The modern fix is scheduler.yield(), which explicitly hands control back to the browser between chunks of work so it can process pending interactions before resuming:
javascript
// BEFORE: One giant blocking function
async function processLargeDataset(items) {
const results = [];
for (const item of items) {
// Each iteration could take 5-10ms
// 1000 items = 5000-10000ms blocking the main thread!
results.push(heavyComputation(item));
}
return results;
}
// AFTER: Yielding to the browser between chunks
async function processLargeDataset(items) {
const results = [];
const CHUNK_SIZE = 50;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
for (const item of chunk) {
results.push(heavyComputation(item));
}
// Yield to the browser β lets it handle pending clicks/inputs
if (typeof scheduler !== 'undefined' && scheduler.yield) {
await scheduler.yield();
} else {
// Fallback for browsers without scheduler.yield()
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return results;
}
For computation-heavy pages, chunking and yielding is usually the largest single INP win you can make, because it converts one blocking 5-second task into dozens of short tasks the browser can interrupt to honor a click.
3. Debounce Event Handlers
Handlers that fire on every keystroke, scroll, or mouse move flood the main thread. Debouncing ensures the expensive work only runs once the user's input settles:
javascript
// Debounce utility β prevents handler firing on every keystroke
function debounce(fn, delay = 150) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// BEFORE: Fires on EVERY keystroke, blocks main thread each time
searchInput.addEventListener('input', (e) => {
const results = searchDatabase(e.target.value); // 100ms+ operation
renderResults(results); // 50ms+ DOM manipulation
});
// AFTER: Only fires 150ms after user stops typing
searchInput.addEventListener('input', debounce((e) => {
const results = searchDatabase(e.target.value);
renderResults(results);
}, 150));
// For scroll handlers, use requestAnimationFrame instead
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
Use debounce for text input and resize, and requestAnimationFrame throttling for scroll β scroll work should align to the frame budget, not a timer.
A developer studying a Chrome DevTools Performance panel showing long task bars and an interaction marker in the flame chart
4. Defer Non-Critical JavaScript
JavaScript running during page load competes with interaction handlers for main-thread time. Defer anything not needed for the first meaningful interaction:
Add defer or async to non-critical <script> tags.
Use dynamic import() to load features only when a user actually triggers them.
Push analytics, chat widgets, and social embeds into requestIdleCallback.
javascript
// Load heavy features only when needed
document.getElementById('open-editor').addEventListener('click', async () => {
// Show immediate visual feedback (< 1ms)
showLoadingSpinner();
// Then load the heavy module
const { RichEditor } = await import('./rich-editor.js');
const editor = new RichEditor('#container');
hideLoadingSpinner();
});
// Defer non-critical work to idle periods
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Analytics, prefetching, non-visible DOM updates
initializeAnalytics();
prefetchNextPageAssets();
lazyLoadBelowFoldImages();
}, { timeout: 2000 }); // 2s max wait
} else {
// Fallback: run after a short delay
setTimeout(() => {
initializeAnalytics();
prefetchNextPageAssets();
}, 1000);
}
Large DOMs make every interaction slower because the browser recalculates styles and layout across more nodes. Aim for a leaner tree β Lighthouse starts warning well before you hit a few thousand elements.
Strategies that work:
Virtualize long lists so you only render the visible rows.
Use `content-visibility: auto` on below-fold sections to skip their rendering until needed.
Remove hidden elements from the DOM entirely rather than leaving them as display: none.
Lazy-render tab content β build the DOM for a tab only when it's activated.
A bloated DOM also drags down layout-shift behavior, which feeds into INP whenever an interaction triggers reflow. If you're fighting both, the CLS fix guide covers the layout side directly.
6. Offload Heavy Computation to Web Workers
For genuinely CPU-intensive work β image processing, large data transforms, complex math β move it off the main thread entirely so it can never block an interaction:
javascript
// main.js β keeps the main thread free for interactions
const worker = new Worker('/js/data-worker.js');
document.getElementById('process-btn').addEventListener('click', () => {
// Immediate visual feedback β this renders in < 16ms
showProgressBar();
// Heavy work happens in background thread
worker.postMessage({ type: 'PROCESS', data: largeDataset });
});
worker.onmessage = (event) => {
if (event.data.type === 'COMPLETE') {
hideProgressBar();
renderResults(event.data.results);
}
};
javascript
// data-worker.js β runs on separate thread, never blocks UI
self.onmessage = (event) => {
if (event.data.type === 'PROCESS') {
const results = heavyComputation(event.data.data);
self.postMessage({ type: 'COMPLETE', results });
}
};
function heavyComputation(data) {
// This can take 500ms+ without affecting INP at all
return data.map(item => complexTransform(item));
}
The key insight: the worker can grind for half a second and your INP stays excellent, because the main thread is free to render the progress bar and respond to clicks the whole time.
7. Optimize Third-Party Scripts
Third-party scripts β analytics, ads, chat widgets, social embeds β are frequent INP killers. They run heavy JavaScript on the main thread and often compete for it precisely when users are interacting.
Audit what each third party costs you in the Performance trace; identify the heaviest scripts.
Load them with async or defer so they don't block first interaction.
Isolate them with Partytown or a web worker so their work happens off the main thread.
Delete scripts you no longer use β that abandoned A/B-testing snippet from 2023 is still executing.
Set fetchpriority="low" on non-critical third-party resources.
For the full performance picture beyond INP, the PageSpeed score guide explains how these scripts also drag down your overall Lighthouse number, and the Technical SEO pillar guide ties Core Web Vitals into the rest of your crawl and indexation health.
Common INP Mistakes to Avoid
Only measuring in lab conditions. Lighthouse uses a simulated throttled CPU; real users on budget Android phones experience worse INP. Always confirm with field data in CrUX/Search Console.
Ignoring layout shifts during interactions. If clicking a button shoves elements around and forces a layout recalculation, that time counts toward INP. Animate with transform and opacity instead of properties that trigger layout.
Synchronous localStorage in event handlers.localStorage.getItem() and setItem() are synchronous and can stall a hot interaction path on slower devices. Keep them out of click and keypress handlers.
Over-relying on full-page hydration. SPAs that hydrate the entire page at once create massive long tasks. Use selective hydration, React.lazy(), or an islands architecture so interactivity arrives in small pieces.
No immediate visual feedback. Even when the real work takes 300 ms, painting a pressed state, spinner, or skeleton within the first frame makes the interaction feel instant. Split every handler into "immediate feedback" plus "actual work."
Two developers reviewing a Core Web Vitals report in Google Search Console on a laptop, pointing at the INP graph
Measuring INP Improvement
After shipping fixes, verify the impact across both lab and field:
CrUX dashboard β 28-day field data at the 75th percentile, the same basis Google ranks on.
`web-vitals` library β drop it into your site for real-user monitoring and immediate feedback before CrUX catches up.
Chrome DevTools Performance tab β lab debugging to confirm long tasks shrank.
SlapMyWeb scanner β run a full audit to track INP alongside every other performance metric.
Field data takes about 28 days to fully reflect your changes in CrUX, so use the web-vitals library for same-day confirmation on real users, then watch Search Console catch up over the following month.
Frequently Asked Questions
What is a good INP score?
A good INP score is 200 milliseconds or less, measured at the 75th percentile of real-user interactions on a page. Between 200 and 500 ms needs improvement, and anything above 500 ms is poor. Because INP reflects roughly your worst interactions, a single slow modal or search box can push an otherwise fast page into the "needs improvement" range.
Does INP affect Google rankings?
Yes. INP replaced FID as the official responsiveness Core Web Vital in March 2024 and is a confirmed signal within Google's page experience system. It rarely outweighs relevance and content quality on its own, but it acts as a tiebreaker β in competitive niches where pages are otherwise comparable, poor INP can cost you position.
What is the difference between INP and FID?
FID only measured the input delay before the first interaction's handler began running. INP measures the full duration β input delay plus processing plus presentation β of all interactions throughout the page's life, then reports close to the worst one. INP is a much harder bar to pass because it captures the complete responsiveness experience, not just the first impression.
Can I fix INP without rewriting my entire codebase?
Usually, yes. The highest-impact INP fixes are surgical: chunking one long task with scheduler.yield(), debouncing one search handler, or deferring one heavy third-party script. Open your Performance trace, find the single longest task that fires during an interaction, and fix that one first β then re-measure before touching anything else.
Which causes worse INP, slow JavaScript or a large DOM?
In most cases long-running JavaScript on the main thread is the bigger offender, because it blocks the browser from handling input at all. A large DOM compounds the problem by making the presentation phase slower, since the browser has more elements to style, lay out, and paint. Fix the long tasks first, then trim the DOM to shave the remaining presentation delay.