Web Performance & Standards

What is INP?

INP, Interaction to Next Paint, is a Core Web Vitals metric that measures the responsiveness of a web page to user interactions, specifically the time from when a user interacts with a page to when the browser next paints a visual update in response to that interaction. INP replaced First Input Delay, FID, as the third Core Web Vital in March 2024, providing a more comprehensive assessment of interaction responsiveness throughout the entire page visit rather than only the first interaction.

An interaction in INP terms is a discrete user action that triggers a visual response, a mouse click on a button, a tap on a mobile screen element, or a keyboard key press on a focusable element. Scrolling and hover events are explicitly excluded from INP measurement, they are handled differently by browsers and have distinct performance characteristics from discrete user actions.

INP measures the entire interaction latency, from the moment the user acts to the moment the browser paints the visual response to that action. This complete measurement encompasses three phases, input delay, the time between the user’s action and when the browser begins processing it, processing time, the time to execute event handlers triggered by the interaction, and presentation delay, the time from when event handlers complete to when the browser finishes painting the updated visual.

INP is the Core Web Vital least directly affected by redirect management, it is a post-load interactivity metric that measures how the page behaves after it has loaded and the user begins interacting with it. Redirects affect the loading phase, LCP and TTFB, but not the post-load interaction responsiveness that INP captures. Understanding INP completes the picture of page experience quality alongside LCP and CLS.

INP vs First Input Delay, why INP replaced FID

First Input Delay, FID, was the original interactivity Core Web Vital before INP replaced it in March 2024. Understanding why INP replaced FID clarifies what INP improves upon and why it is a better measure of interaction responsiveness.

What FID measured, the delay between a user’s first interaction with a page and when the browser could begin processing that interaction. FID measured only the input delay component, the queue time before the event handler started, not the time to actually execute the handler or paint the visual response. FID also measured only the first interaction on the page, missing interactions that occur throughout the rest of the visit.

FID’s limitations:

Measuring only the first interaction, many pages have poor responsiveness for interactions that occur after the initial page load, when long-running JavaScript tasks are still executing, but recover to good responsiveness once the page is fully loaded. FID only captured the initial load-time responsiveness problem, missing poor responsiveness during the full page experience.

Measuring only input delay, FID excluded the processing time of event handlers and the rendering time after handlers completed. A page where the first interaction had zero input delay but the event handler executed for 500ms would have a perfect FID score, despite the user experiencing a 500ms delay before seeing any visual response.

What INP improves upon, INP measures the complete interaction latency, from user action to visual paint, rather than only the input delay component. INP also observes all interactions throughout the page visit, not just the first, and reports the worst interaction latency as the page score, with outlier exclusion for pages with many interactions. This comprehensive measurement better represents the real user experience of interaction responsiveness.

INP thresholds

Google defines three performance categories for INP, determining how a page is assessed in the Page Experience ranking system.

Good, under 200 milliseconds, pages where interactions are completed and the visual response is painted within 200ms are considered responsive. The 200ms threshold reflects research showing that users perceive responses under 200ms as immediate, creating the sensation of a direct, instantaneous response to their action. To achieve a good INP assessment 75% of real user interactions, as measured by Chrome User Experience Report, must complete within 200ms.

Needs Improvement, 200 to 500 milliseconds, interactions that take 200 to 500ms to produce a visual response are perceptibly delayed. Users notice the lag between their action and the visual response, the page feels slightly sluggish. Pages in this range should be prioritised for interaction responsiveness improvements.

Poor, over 500 milliseconds, interactions taking over 500ms feel severely unresponsive. Users may repeat their interaction believing it did not register, triggering multiple events, or may abandon the interaction entirely. Poor INP creates a genuinely frustrating interactive experience.

How INP is measured

INP measurement involves observing all qualifying interactions throughout a page visit and reporting the worst interaction latency, with statistical outlier handling.

Qualifying interactions, INP counts three types of user interactions:

Mouse clicks, clicking with a mouse or trackpad on interactive elements. Touch taps, tapping on a touchscreen on mobile devices. Key presses, pressing keyboard keys on focused elements, inputs, buttons, interactive components.

Scroll events are explicitly excluded, they are handled by browsers through separate mechanisms and have distinct performance characteristics.

Interaction phases:

Input delay, the time between the user’s action and when the browser begins running the interaction’s event handlers. Input delay occurs when the main thread is occupied with other work, long JavaScript tasks, at the time of the interaction. The interaction is queued and waits for the main thread to become available. Input delay is the same concept FID measured, but INP includes it as one component of the complete interaction latency.

Processing time, the time to execute the event handlers triggered by the interaction. Complex event handlers that perform expensive operations, large DOM manipulations, synchronous layout queries, data processing, extend processing time. Processing time can be substantial even when input delay is minimal.

Presentation delay, the time between when event handlers complete and when the browser finishes rendering the updated visuals. The browser must style, layout, and paint the changes triggered by the event handlers. Complex visual updates, affecting large portions of the page, triggering reflow, extend presentation delay.

Total INP = Input Delay + Processing Time + Presentation Delay

Outlier handling, for pages with many interactions INP applies outlier exclusion, the worst 1 in 50 interactions is excluded from the INP calculation. This prevents a single anomalous interaction from dominating the score for pages with extensive interaction. For pages with fewer than 50 interactions no outlier exclusion is applied.

INP as the reported metric, the INP score for a page visit is the worst interaction latency remaining after outlier exclusion, the highest individual interaction latency observed. This worst-case reporting encourages pages to eliminate their slowest interactions rather than improving their average while leaving occasional terrible interactions.

What causes poor INP

Poor INP is caused by work that blocks the browser’s main thread, preventing the browser from processing interactions promptly and rendering visual responses quickly.

Long JavaScript tasks, the primary cause of poor INP. A long task is any JavaScript execution that occupies the main thread for more than 50 milliseconds continuously. When a user interacts during a long task the interaction is queued, waiting for the long task to complete before the event handler can run. A 500ms long task means an interaction that occurs during it has at least 500ms of input delay, producing poor INP regardless of how fast the event handler itself executes.

Long tasks arise from: Large synchronous JavaScript execution, processing significant data, complex algorithms, serialising and parsing large objects, all synchronously on the main thread. Forced synchronous layouts, reading layout properties, offsetWidth, getBoundingClientRect, immediately after making DOM changes forces the browser to recalculate layout synchronously, a performance-expensive operation. Large DOM sizes, pages with many thousands of DOM elements require more work to style, layout, and paint, extending presentation delay for any interaction.

Inefficient event handlers, event handlers that do too much work synchronously in response to an interaction. A click handler that performs a complex DOM traversal, makes synchronous API calls, or executes expensive algorithms extends processing time, delaying the visual response even after the main thread becomes available for the event.

Third-party scripts, analytics platforms, advertising systems, social sharing widgets, live chat scripts, and other third-party JavaScript execute on the same main thread as the page’s own code. Heavy third-party scripts that execute frequently or perform expensive operations degrade INP by occupying the main thread and creating input delay for user interactions.

Third-party scripts are particularly problematic because they are outside the page developer’s direct control, they execute their own code on the shared main thread. Auditing third-party script performance and removing scripts with high performance costs relative to their value is an important INP optimisation.

Large DOM sizes, the Document Object Model, the browser’s representation of the page structure, grows with every element on the page. Large DOMs, pages with many thousands of elements, make styling, layout, and paint operations more expensive. Every interaction that triggers visual changes requires the browser to recalculate styles and layouts for affected elements, larger DOMs mean more expensive recalculations.

Rendering bottlenecks, complex CSS selectors, expensive visual effects, box shadows, filters, gradients on large areas, and overlapping transparent layers all increase the presentation delay component of INP. Even with fast event handler execution the browser may take significant time to paint the visual update if rendering is expensive.

Improving INP

INP improvement focuses on reducing main thread blocking and making event handlers more efficient, techniques that differ significantly from LCP and CLS improvements.

Break up long tasks, the most impactful INP improvement strategy. Long tasks that block the main thread create input delay. Breaking long tasks into smaller chunks, yielding to the browser between chunks, allows interactions to be processed during the gaps.

// Long synchronous task — causes poor INP
function processLargeDataset(data) {
    for (const item of data) {
        processItem(item)  // Expensive processing
    }
    updateUI()
}

// Chunked with yielding — better INP
async function processLargeDatasetChunked(data) {
    const chunkSize = 50
    for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize)
        for (const item of chunk) {
            processItem(item)
        }
        // Yield to the browser between chunks
        await new Promise(resolve => setTimeout(resolve, 0))
    }
    updateUI()
}
// Long synchronous task — causes poor INP
function processLargeDataset(data) {
    for (const item of data) {
        processItem(item)  // Expensive processing
    }
    updateUI()
}

// Chunked with yielding — better INP
async function processLargeDatasetChunked(data) {
    const chunkSize = 50
    for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize)
        for (const item of chunk) {
            processItem(item)
        }
        // Yield to the browser between chunks
        await new Promise(resolve => setTimeout(resolve, 0))
    }
    updateUI()
}
// Long synchronous task — causes poor INP
function processLargeDataset(data) {
    for (const item of data) {
        processItem(item)  // Expensive processing
    }
    updateUI()
}

// Chunked with yielding — better INP
async function processLargeDatasetChunked(data) {
    const chunkSize = 50
    for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize)
        for (const item of chunk) {
            processItem(item)
        }
        // Yield to the browser between chunks
        await new Promise(resolve => setTimeout(resolve, 0))
    }
    updateUI()
}

Use the Scheduler API for task prioritisation, the scheduler.yield() API, available in modern browsers, provides a clean yielding mechanism that prioritises pending user interactions:

async function processWithYield(data) {
    for (const item of data) {
        processItem(item)
        
        // Yield — allows pending interactions to be processed
        if (navigator.scheduling?.isInputPending()) {
            await scheduler.yield()
        }
    }
}
async function processWithYield(data) {
    for (const item of data) {
        processItem(item)
        
        // Yield — allows pending interactions to be processed
        if (navigator.scheduling?.isInputPending()) {
            await scheduler.yield()
        }
    }
}
async function processWithYield(data) {
    for (const item of data) {
        processItem(item)
        
        // Yield — allows pending interactions to be processed
        if (navigator.scheduling?.isInputPending()) {
            await scheduler.yield()
        }
    }
}

Defer non-critical JavaScript, JavaScript that does not need to run during page load should be deferred. Using defer and async attributes on script tags, dynamically loading scripts after the page is interactive, and code splitting, loading only the JavaScript needed for the current page, all reduce the JavaScript executing during initial load that creates input delay for early interactions.

Audit and reduce third-party scripts, systematically evaluate every third-party script loaded on the page. Use Chrome DevTools Performance panel to identify which third-party scripts are executing during interactions and how much main thread time they consume. Remove scripts with high cost and low value. Load necessary scripts with minimal main thread impact, using workers or requestIdleCallback for non-urgent processing.

Minimise DOM size, reduce the number of DOM elements by removing unnecessary wrapper elements, using CSS to achieve layout without DOM structure, and implementing virtual lists, rendering only the visible portion of long lists rather than all elements simultaneously.

Optimise event handlers, make event handlers as fast as possible by deferring non-critical work, avoiding synchronous layout queries immediately after DOM mutations, and using requestAnimationFrame for visual updates:

// Bad — synchronous layout query after DOM change
button.addEventListener('click', () => {
    container.classList.add('expanded')
    const height = container.offsetHeight  // Forces synchronous layout
    console.log('New height:', height)
})

// Better — read layout before mutation or defer to rAF
button.addEventListener('click', () => {
    requestAnimationFrame(() => {
        container.classList.add('expanded')
    })
})
// Bad — synchronous layout query after DOM change
button.addEventListener('click', () => {
    container.classList.add('expanded')
    const height = container.offsetHeight  // Forces synchronous layout
    console.log('New height:', height)
})

// Better — read layout before mutation or defer to rAF
button.addEventListener('click', () => {
    requestAnimationFrame(() => {
        container.classList.add('expanded')
    })
})
// Bad — synchronous layout query after DOM change
button.addEventListener('click', () => {
    container.classList.add('expanded')
    const height = container.offsetHeight  // Forces synchronous layout
    console.log('New height:', height)
})

// Better — read layout before mutation or defer to rAF
button.addEventListener('click', () => {
    requestAnimationFrame(() => {
        container.classList.add('expanded')
    })
})

Web Workers for background processing, move non-rendering computation, data processing, parsing, calculations, to Web Workers, background threads that do not affect the main thread. Main thread work is limited to DOM manipulation and rendering, which cannot be done from workers, while all heavy computation executes off the main thread without causing input delay.

Measuring INP in practice

Chrome DevTools Performance panel, the most detailed INP diagnostic tool. Recording a performance profile while interacting with the page captures all main thread activity, showing long tasks, event handler execution times, and rendering work. The Interactions track shows INP events with their duration broken down into input delay, processing time, and presentation delay components.

Web Vitals Chrome extension, displays real-time INP values while browsing, updating as interactions occur. Useful for quick INP checks during development and for identifying which specific interactions are causing poor INP.

Lighthouse, provides Total Blocking Time, TBT, as a lab proxy for INP, long tasks that contribute to TBT also contribute to INP. High TBT in Lighthouse correlates with poor INP in field data. The Reduce JavaScript execution time and Minimize main thread work audits identify specific sources of main thread blocking.

PerformanceObserver API, directly measuring INP in JavaScript:

const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.interactionId) {
            console.log('Interaction duration:', entry.duration)
            console.log('Input delay:', entry.processingStart - entry.startTime)
            console.log('Processing time:', entry.processingEnd - entry.processingStart)
            console.log('Presentation delay:', entry.duration - entry.processingEnd + entry.startTime)
        }
    }
})

observer.observe({ type: 'event', buffered: true, durationThreshold: 16 })
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.interactionId) {
            console.log('Interaction duration:', entry.duration)
            console.log('Input delay:', entry.processingStart - entry.startTime)
            console.log('Processing time:', entry.processingEnd - entry.processingStart)
            console.log('Presentation delay:', entry.duration - entry.processingEnd + entry.startTime)
        }
    }
})

observer.observe({ type: 'event', buffered: true, durationThreshold: 16 })
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.interactionId) {
            console.log('Interaction duration:', entry.duration)
            console.log('Input delay:', entry.processingStart - entry.startTime)
            console.log('Processing time:', entry.processingEnd - entry.processingStart)
            console.log('Presentation delay:', entry.duration - entry.processingEnd + entry.startTime)
        }
    }
})

observer.observe({ type: 'event', buffered: true, durationThreshold: 16 })

Related terms

Related terms

Ready to keep every link alive?

Ready to keep every link alive?

Ready to keep every link alive?