What Is INP and How Do You Fix It? 2026 Developer Guide
Learn what Interaction to Next Paint (INP) is and how to fix INP in 2026. Break up long tasks, optimize event handlers, and reduce main-thread blocking latency.
A mobile user browses an e-commerce catalog, selects a product variant, and taps the "Add to Cart" button. Nothing happens. Assuming the screen missed their tap, they press the button two more times. Four hundred milliseconds later, the main thread finally unclogs, executing the queued callbacks simultaneously, opening three modal dialogs, and adding three duplicate items to the checkout cart.
This frustrating interaction lag is the exact user experience failure that Interaction to Next Paint (INP) was designed to quantify. When a browser's main thread is monopolized by monolithic JavaScript bundles, complex React re-renders, or heavy third-party tracking beacons, user interactions stall. To fix INP and deliver instantaneous visual feedback, frontend engineers must understand the three distinct phases of user latency, break up long tasks, and optimize the browser rendering pipeline.
In this deep technical guide, you will learn the exact mechanics of INP, diagnose the architectural bottlenecks that cause main-thread freezing, apply proven JavaScript code patterns to yield execution, and automate responsiveness auditing across your entire site.
Understanding INP: The Evolution from FID to Full-Lifecycle Responsiveness
Interaction to Next Paint (INP) is an official Core Web Vital metric that evaluates the overall responsiveness of a web page throughout its entire lifecycle. In March 2024, Google officially replaced First Input Delay (FID) with INP across all search ranking algorithms and Core Web Vitals assessments.
+-------------------------------------------------------------------------+
| CORE WEB VITALS: INP SCORING THRESHOLDS |
+-----------------------------+-----------------------------+-------------+
| GOOD (Passing) | NEEDS IMPROVEMENT | POOR |
| Latency <= 200 ms | 200 ms < Latency <= 500 ms | > 500 ms |
| (Green) | (Amber) | (Red) |
+-----------------------------+-----------------------------+-------------+
| Evaluated at the 75th percentile of all mobile and desktop page loads. |
+-------------------------------------------------------------------------+To achieve a "Good" rating, 75% of all page visits must register an INP score of 200 milliseconds or less. If you are new to the metric, review our foundational INP Interaction to Next Paint explained guide for a conceptual overview, or our broader Core Web Vitals explainer.
Why First Input Delay (FID) Was Inadequate
First Input Delay only measured the Input Delay of the very first user interaction (typically a click or tap during initial page load). Once the browser started running the event listener, FID stopped recording.
+-------------------------------------------------------------------------+
| FID VS INP: COVERAGE COMPARISON |
+------------------------------------+------------------------------------+
| FIRST INPUT DELAY (FID) - Legacy | INTERACTION TO NEXT PAINT (INP) |
+------------------------------------+------------------------------------+
| Measures only the 1st interaction | Measures ALL interactions on page |
| Measures only Input Delay | Measures Input Delay + Processing |
| | + Presentation Delay (Next Paint) |
| Ignores runtime SPA interactions | Evaluates full session lifecycle |
| Easy to pass artificially (90%+) | Reflects true perceived latency |
+------------------------------------+------------------------------------+FID allowed slow applications to pass Core Web Vitals if their first click happened after initial hydration, even if every subsequent tap on an accordion, checkout form, or mobile navigation menu froze the screen for 600ms. INP captures the worst interaction (or 98th percentile for pages with dozens of interactions) over the entire user visit.
Which Interactions Are Observed by INP?
According to the official web.dev INP specification, INP observes discrete user-initiated actions:
- Mouse Clicks: Primary button clicks on interactive elements.
- Taps / Touches: Touchscreen interactions on mobile devices and tablets.
- Keypresses: Physical or virtual keyboard inputs (
keydown,keyup).
Note: Continuous gestures like mouse movement, hover states, scrolling, and pinch-to-zoom are excluded from INP calculations because they rely on dedicated compositor-driven scrolling pipelines.
The Anatomy of an Interaction: The 3 Distinct Phases of INP
Every discrete user action consists of three sequential phases. To optimize an interaction, you must identify which of these three phases is consuming excessive time.
+-------------------------------------------------------------------------+
| THE THREE PHASES OF AN INTERACTION |
| |
| User Taps Button |
| | |
| v |
| [================ PHASE 1: INPUT DELAY ================] |
| Time elapsed while the browser waits for the main thread to become |
| idle so it can dispatch the event listener. |
| | |
| v |
| [============== PHASE 2: PROCESSING DURATION ============] |
| Time consumed running the JavaScript event listener callbacks, state |
| mutations, computations, and DOM modifications. |
| | |
| v |
| [============== PHASE 3: PRESENTATION DELAY ============] |
| Time required for the browser to recalculate styles, perform layout |
| reflow, composite layers, and paint the new frame to the screen. |
| | |
| v |
| Next Frame Committed to Screen (User Sees Visual Feedback) |
| |
| TOTAL INP LATENCY = Input Delay + Processing Time + Presentation Delay |
+-------------------------------------------------------------------------+Inspecting Interaction Phases with the Event Timing API
Modern browsers expose the PerformanceEventTiming API, allowing developers to programmatically extract the exact breakdown of any interaction:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId) {
const inputDelay = entry.processingStart - entry.startTime;
const processingTime = entry.processingEnd - entry.processingStart;
const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
console.table({
'Interaction ID': entry.interactionId,
'Target Element': entry.target?.tagName || 'Unknown',
'Total Duration (INP)': `${entry.duration.toFixed(1)} ms`,
'1. Input Delay': `${inputDelay.toFixed(1)} ms`,
'2. Processing Time': `${processingTime.toFixed(1)} ms`,
'3. Presentation Delay': `${presentationDelay.toFixed(1)} ms`,
});
}
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });Strategy 1: Breaking Up Long Tasks on the Main Thread
The primary reason for elevated Input Delay is that the main thread is occupied executing a Long Task.
Under the PerformanceLongTaskTiming API, a "Long Task" is defined as any uninterrupted JavaScript execution on the main thread that exceeds 50 milliseconds.
+-------------------------------------------------------------------------+
| MAIN THREAD MONOPOLY: THE 50MS CEILING |
| |
| Frame Budget at 60 FPS: ~16.6 ms |
| |
| [==== Task A: 240ms (LONG TASK) ====] |
| ^ |
| | User clicks screen at 80ms mark |
| +--- Browser CANNOT handle click until Task A finishes! |
| Input Delay = 240ms - 80ms = 160ms (Instant INP Fail) |
| |
| AFTER TASK CHUNKING WITH YIELDING: |
| [Chunk 1: 30ms] [Chunk 2: 30ms] [Chunk 3: 30ms] [Chunk 4: 30ms] |
| ^ |
| | User clicks at 45ms mark |
| +--- Browser handles click after Chunk 2 (15ms) |
| Input Delay = 15ms (Passes with 92% buffer) |
+-------------------------------------------------------------------------+Modern Main-Thread Yielding with scheduler.yield()
In the past, developers relied on setTimeout(fn, 0) or requestAnimationFrame hacks to split work. However, setTimeout places remaining work at the absolute back of the task queue behind other background scripts and timer callbacks.
Modern browsers provide the dedicated Scheduler API scheduler.yield(). Calling await scheduler.yield() pauses execution, hands control back to the browser's event loop to render frames and handle pending user inputs, and then resumes execution with prioritized continuation.
// Progressive Yielding Helper with Universal Browser Fallback
async function yieldToMain() {
if ('scheduler' in window && 'yield' in window.scheduler) {
return window.scheduler.yield();
}
// Fallback for browsers without native scheduler.yield support
return new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () => resolve();
channel.port2.postMessage(null);
});
}
// Processing a Large Dataset Without Freezing User Interactions
async function processInventoryItems(items) {
let lastYieldTime = performance.now();
for (let i = 0; i < items.length; i++) {
// Perform data transformation
transformItemRecord(items[i]);
// Check if task has occupied the main thread for more than 40ms
if (performance.now() - lastYieldTime > 40) {
await yieldToMain();
lastYieldTime = performance.now();
}
}
}Before vs After: Long Task Chunking
+-------------------------------------------------------------------------+
| PERFORMANCE AUDIT: INVENTORY DATA FILTER |
+--------------------------+-----------------------+----------------------+
| Execution Mode | Worst Interaction INP | Longest Task Duration|
+--------------------------+-----------------------+----------------------+
| Monolithic Synchronous | 342 ms (Poor) | 285 ms |
| Chunked with `yield()` | 38 ms (Good) | 32 ms |
+--------------------------+-----------------------+----------------------+Strategy 2: Optimizing Event Handlers and State Updates
High Processing Duration occurs when the JavaScript event callback itself attempts to execute heavy computations, API formatting, or massive DOM tree re-renders synchronously before releasing the main thread.
ANTI-PATTERN:
User clicks "Apply Filter"
-> Click Handler executes (180ms)
-> Computes 5,000 regex matches
-> Re-renders 300 DOM card elements
-> Fires 4 marketing tracking payloads
-> Browser paints next frame (Total Latency: 220ms -> FAIL)
OPTIMAL PATTERN:
User clicks "Apply Filter"
-> Click Handler executes (8ms)
-> Updates button to "Filtering..." loading state
-> Browser paints next frame (INP: 24ms -> PASS)
-> Subsequent background task computes filter & renders cardsDecoupling Urgent Visual Feedback from Heavy Processing
To fix INP, prioritize the immediate visual confirmation of user intent within the very first render frame, deferring heavy computation to subsequent frames.
const filterButton = document.querySelector('#apply-filters');
filterButton.addEventListener('click', async (event) => {
// 1. URGENT: Provide immediate tactile feedback in frame 1
filterButton.classList.add('is-loading');
filterButton.setAttribute('aria-busy', 'true');
// 2. Yield control immediately to allow the browser to paint the loading state
await yieldToMain();
// 3. NON-URGENT: Execute intensive filter calculations
const filteredData = computeComplexFilters();
// 4. Update the DOM with the computed results
renderResultsGrid(filteredData);
filterButton.classList.remove('is-loading');
filterButton.removeAttribute('aria-busy');
});Leveraging React 18+ Transitions (startTransition)
In React single-page applications, updating complex state synchronously blocks the main thread while React reconciles the virtual DOM. Wrap non-urgent state updates in startTransition to mark them as interruptible.
import React, { useState, useTransition } from 'react';
export function ProductCatalog({ allProducts }) {
const [searchQuery, setSearchQuery] = useState('');
const [filteredList, setFilteredList] = useState(allProducts);
const [isPending, startTransition] = useTransition();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// URGENT: Controlled input must reflect typed character immediately
setSearchQuery(value);
// NON-URGENT: Mark expensive list filtering as a transition
startTransition(() => {
const results = allProducts.filter((item) =>
item.title.toLowerCase().includes(value.toLowerCase())
);
setFilteredList(results);
});
};
return (
<div className="catalog-container">
<input
type="search"
value={searchQuery}
onChange={handleInputChange}
placeholder="Search 10,000 components..."
/>
{isPending && <div className="loading-spinner">Searching...</div>}
<ProductGrid items={filteredList} />
</div>
);
}Strategy 3: Offloading Intensive Computations to Web Workers
When computational operations (such as parsing massive CSV files, running client-side fuzzy search algorithms, encoding audio/images, or complex cryptography) take hundreds of milliseconds, yielding is insufficient. The computation must be moved entirely off the UI thread.
The Web Workers API runs scripts in background threads, completely isolating heavy calculations from the browser's rendering engine.
+-------------------------------------------------------------------------+
| MAIN THREAD VS WEB WORKER ARCHITECTURE |
| |
| MAIN UI THREAD: |
| +-------------------------------------------------------------------+ |
| | User Input Events -> Paint Frame (16.6ms) -> Input Handling (0ms) | |
| | (100% Responsive, Zero Freezing, Smooth 60 FPS Animations) | |
| +-------------------------------------------------------------------+ |
| | ^ |
| | postMessage(query) postMessage(results)| |
| v | |
| BACKGROUND WEB WORKER THREAD: |
| +-------------------------------------------------------------------+ |
| | Heavy CPU Loop: Fuzzy String Search Across 50,000 Data Objects | |
| | Execution Time: 420 ms (Zero impact on Main Thread UI) | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+Implementing a Dedicated Search Worker
1. Worker Script (search.worker.js)
// search.worker.js - Runs in background thread
self.onmessage = function(event) {
const { dataset, query } = event.data;
// Heavy computation executing without blocking the main UI
const matches = dataset.filter(item => {
return item.name.toLowerCase().includes(query.toLowerCase()) ||
item.sku.includes(query);
});
self.postMessage({ results: matches });
};2. Main Thread Client (app.js)
// app.js - Runs on Main UI Thread
const worker = new Worker(new URL('./search.worker.js', import.meta.url));
const searchInput = document.querySelector('#site-search');
const resultsContainer = document.querySelector('#results-view');
searchInput.addEventListener('input', (event) => {
const query = event.target.value;
// Hand off search payload to background worker
worker.postMessage({
dataset: window.CACHED_PRODUCT_CATALOG,
query: query
});
});
worker.onmessage = (event) => {
const { results } = event.data;
renderSearchResults(results);
};Measured Real-World Impact
+-------------------------------------------------------------------------+
| PERFORMANCE AUDIT: 50,000-ROW DATA FILTER |
+--------------------------+-----------------------+----------------------+
| Thread Architecture | INP Metric Score | Main Thread Blocking |
+--------------------------+-----------------------+----------------------+
| Main Thread Synchronous | 485 ms (Poor) | 460 ms |
| Web Worker Background | 22 ms (Good) | 0 ms |
+--------------------------+-----------------------+----------------------+Strategy 4: Minimizing DOM Complexity and Presentation Delay
Even if your JavaScript handler completes in under 10ms, your interaction can still fail INP due to Presentation Delay.
Presentation Delay represents the time required for the browser to recalculate computed CSS styles, perform layout tree reflow, and composite paint layers. If a page has an oversized DOM tree (e.g., $> 2,000$ DOM elements), even a trivial class change can trigger a layout calculation lasting over 150ms.
+-------------------------------------------------------------------------+
| PREVENTING FORCED SYNCHRONOUS LAYOUTS |
| |
| BAD: Interleaving Reads and Writes (Layout Thrashing) |
| ----------------------------------------------------- |
| elementA.style.height = '100px'; // Write (Invalidates layout) |
| const h1 = elementB.offsetHeight; // Read (FORCES IMMEDIATE REFLOW)|
| elementA.style.width = '200px'; // Write (Invalidates layout) |
| const h2 = elementC.offsetHeight; // Read (FORCES IMMEDIATE REFLOW)|
| |
| GOOD: Batching Reads First, Writes Second |
| ----------------------------------------- |
| const h1 = elementB.offsetHeight; // Read (Cached layout) |
| const h2 = elementC.offsetHeight; // Read (Cached layout) |
| elementA.style.height = '100px'; // Write |
| elementA.style.width = '200px'; // Write |
| // Browser executes single reflow at next frame boundary! |
+-------------------------------------------------------------------------+Best Practices for Minimizing Presentation Delay:
- Reduce Total DOM Depth: Keep total DOM nodes below 1,500 and maximum nesting depth under 32 levels.
- Use CSS
content-visibility: auto: Bypasses off-screen rendering calculations, reducing the active layout tree size. - Avoid Modifying Top/Left Coordinates: Animate UI elements using GPU-accelerated
transformandopacityproperties. For an in-depth breakdown of compositor animations, explore our guide on how to fix Cumulative Layout Shift (CLS).
Strategy 5: Auditing and Taming Third-Party Scripts and Tag Managers
Third-party tags—including tag managers, heatmaps, analytics trackers, customer chat widgets, and retargeting pixels—are the most common external source of high INP latency.
Many third-party marketing SDKs attach global, un-debounced event listeners to window or document. When a user clicks anywhere on the page, half a dozen vendor scripts execute synchronous tracking routines simultaneously, stealing the main thread before your application's actual UI handler can run.
+-------------------------------------------------------------------------+
| THIRD-PARTY EVENT LISTENER COLLISION |
| |
| User Clicks "Checkout" Button |
| | |
| +---> [Tag Manager Click Listener]: Parses DOM tags (65ms) |
| +---> [Analytics SDK Listener]: Serializes cookies (45ms) |
| +---> [Heatmap Tracking Script]: Computes mouse coords (50ms) |
| +---> [Chat Widget Beacon]: Checks user session (40ms) |
| | |
| Total Third-Party Overhead: 200ms Blocking Latency |
| Your Application Handler Runs AFTER 200ms -> Instant INP Failure |
+-------------------------------------------------------------------------+How to Tame Third-Party Script Latency:
- Utilize
requestIdleCallbackfor Analytics: Ensure tracking calls execute during idle browser periods rather than directly within interaction handlers. - Consolidate Event Delegation: Audit your Google Tag Manager container. Replace multiple generic "All Clicks" triggers with specific, ID-targeted triggers.
- Leverage
navigator.sendBeacon(): Use asynchronous, non-blocking beacon APIs for data transmission so network requests never delay UI thread commits.
// Non-blocking Analytics Transmission Example
function trackUserInteraction(eventName, metadata) {
const payload = JSON.stringify({ event: eventName, meta: metadata, timestamp: Date.now() });
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics-collector', payload);
} else {
// Fallback using non-blocking fetch with keepalive
fetch('/api/analytics-collector', {
method: 'POST',
body: payload,
keepalive: true,
headers: { 'Content-Type': 'application/json' }
});
}
}How BugViso Diagnoses Main-Thread Long Tasks and INP Bottlenecks
Diagnosing interaction latency in local development on an overpowered developer workstation rarely reflects real-world user conditions. Users on mid-tier mobile hardware with throttled CPU cores experience significantly higher interaction delays.
+-------------------------------------------------------------------------+
| BUGVISO PERFORMANCE & INP DIAGNOSTIC PIPELINE |
| |
| [Target URL Submitted] |
| | |
| v |
| [Headless Chromium Multi-Page Crawl Engine] |
| | |
| +---> 1. Core Web Vitals & Event Timing Engine |
| | (Captures INP, TTFB, LCP via web-vitals IIFE) |
| | (Evaluates Desktop vs Mobile Pixel 5 Device) |
| | |
| +---> 2. CPU & Network Emulation Simulator |
| | (Executes under Slow 3G / Fast 3G + 4x CPU throttle)|
| | |
| +---> 3. Main-Thread Long-Task & TBT Diagnostic Engine |
| | (Captures all tasks > 50ms via PerformanceObserver) |
| | (Attributes longest task to exact script file) |
| | |
| +---> 4. Precise CDP JS Code Coverage Analyzer |
| | (Pinpoints unused script bloat > 40% threshold) |
| | |
| v |
| [Prioritized Remediation Playbook + Branded PDF Executive Audit Report]|
+-------------------------------------------------------------------------+When you run an automated website scan with BugViso, the background worker executes a comprehensive performance audit targeting script execution and main-thread bottlenecks:
- Core Web Vitals Engine: Captures real performance metrics using the self-hosted
web-vitalslibrary and nativePerformanceObserveracross both desktop and mobile device emulation (emulating a Google Pixel 5). - Speed & Performance Simulation Engine (Throttled Hardware Pass): Re-loads the target page under CDP-emulated Slow 3G (400ms RTT, 500 Kbps) and Fast 3G (150ms RTT, 1.6 Mbps) conditions with hardware CPU throttling, exposing how long tasks escalate on budget mobile processors.
- Long-Task Breakdown and Total Blocking Time (TBT): Records all main-thread tasks exceeding 50ms, calculates cumulative Total Blocking Time, and attributes the single worst offending execution block to its source script filename and line duration.
- Precise Code Coverage Analysis: Measures exact byte-level unused JavaScript shipped across your initial bundles, identifying dead dependencies that inflate parse and compile overhead.
- Actionable Remediation Playbook: Surfaces detected script bottlenecks with clear, developer-facing fix actions and millisecond breakdowns in both the interactive dashboard and downloadable executive PDF report.
The checks behind this are covered on the Core Web Vitals audit page.
Common Mistakes When Attempting to Fix INP
Avoid these frequent architectural traps when optimizing for interaction responsiveness:
| Common Mistake | Consequence |
|---|---|
Blind Reliance on setTimeout(0) Queues work behind lower-priority tasks | |
| Optimizing Only First Click Misses late-session interaction lag | |
| Testing Only on Fast CPUs Fails to detect mobile 4x CPU slowdowns | |
| Ignoring Presentation Delay Fast JS still fails if DOM reflow is 200ms |
1. Blindly Using setTimeout(fn, 0)
setTimeout(fn, 0) is not a true scheduler. When you call setTimeout, the browser places the callback in the timer macro-task queue. If there are other queued tasks, network events, or rendering steps, your callback may experience unpredictable delays. Use scheduler.yield() or MessageChannel for deterministic cooperative scheduling.
2. Testing Exclusively on High-End Developer Laptops
Testing your application on an Apple M-series MacBook or an Intel Core i9 workstation with 64GB of RAM will mask virtually all main-thread long tasks. Always test in Chrome DevTools using 4x or 6x CPU throttling and mobile device emulation to replicate real-world consumer conditions.
3. Neglecting Presentation Delay
Many developers spend days optimizing JavaScript execution times down to 10ms, only to discover their INP score remains over 300ms. If modifying the DOM forces the browser to recalculate styles across 3,000 un-contained elements, the resulting Presentation Delay will single-handedly fail your Core Web Vitals audit.
Frequently Asked Questions About Interaction to Next Paint
What is the difference between INP and FID?
First Input Delay (FID) only measured the input delay of the first interaction on a page. Interaction to Next Paint (INP) measures the full duration (Input Delay + Processing Time + Presentation Delay) of all click, tap, and keypress interactions throughout the entire session lifecycle, reporting the worst overall score.
What is considered a good INP score in Core Web Vitals?
A good INP score is 200 milliseconds or less. An INP between 200ms and 500ms indicates that your page "Needs Improvement," while any score exceeding 500ms is rated "Poor." To pass Google's assessment, at least 75% of page visits must achieve an INP under 200ms.
Why does my website pass INP on desktop but fail on mobile devices?
Mobile devices have significantly weaker CPU single-core performance compared to desktop computers. JavaScript execution, garbage collection, and style recalculations take 3x to 5x longer on mobile processors. Additionally, mobile viewports trigger touch event sequences (touchstart, touchend, click) that add extra processing overhead.
Do scrolling and pinch-to-zoom count toward INP?
No. Continuous gestures like scrolling, page panning, and pinch-to-zoom are handled by the browser's dedicated compositor thread and are excluded from INP calculations. INP specifically measures discrete user inputs: mouse clicks, touchscreen taps, and keyboard key presses.
How can I measure INP locally without waiting for Search Console field data?
You can measure INP locally using the official web-vitals JavaScript library or by monitoring the Performance panel in Chrome DevTools. In the DevTools Performance panel, record a session while interacting with your page; any interaction exceeding 200ms will be highlighted with red flags under the "Interactions" track.
Summary and Action Plan
Fixing Interaction to Next Paint requires a proactive, multi-layered optimization strategy: break up long tasks using scheduler.yield(), decouple immediate visual state confirmations from heavy data processing, offload intensive computations to Web Workers, reduce total DOM tree complexity, and audit third-party script execution.
Consistently maintaining responsive interactions across thousands of dynamic pages and diverse mobile hardware requires continuous performance monitoring, which is why running a comprehensive BugViso site scan profiles main-thread script execution and pinpoints long tasks before they impact your field metrics.
See where your site stands
Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.