What You'll Learn
- What INP measures and how it differs from FID (First Input Delay)
- The three sub-parts of INP: input delay, processing time, and presentation delay
- What long tasks are and how they block user interaction responsiveness
- How to break up long tasks using yielding techniques
- How to reduce JavaScript bundle size and execution time
- How third-party scripts contribute to poor INP and how to audit them
- How to diagnose INP problems using Chrome DevTools and real field data
What is INP and How it Differs from FID
Interaction to Next Paint (INP) measures the latency of all user interactions on a page throughout an entire visit and reports the worst-case interaction — or the 98th percentile for pages with many interactions. An "interaction" includes clicks, taps, and keyboard key presses. It does not include scrolling or hovering, which are handled by the browser's compositor thread independently.
INP replaced First Input Delay (FID) as a Core Web Vital in March 2024. FID only measured the delay before the browser started processing the first interaction on a page — it ignored processing time and presentation delay, and only measured one interaction per page visit. A page could have an excellent FID score but sluggish interactions throughout the rest of the session. INP captures the full picture.
Good INP threshold
75th percentile of interactions must be 200ms or under
Needs improvement
Above 500ms is classified as Poor — significant user frustration
Long task threshold
Any main thread task over 50ms can block interaction responses
Sub-part 1
Input Delay
Time from user interaction until the browser starts running the event handlers. Caused by long tasks on the main thread at the time of interaction.
Sub-part 2
Processing Time
Time for event handlers to run. Caused by expensive JavaScript executing synchronously in response to the interaction.
Sub-part 3
Presentation Delay
Time for the browser to render and paint the visual update after event handlers complete. Caused by complex style calculations, layout reflows, or large DOM sizes.
Long Tasks: The Root Cause of Poor INP
A long task is any JavaScript task on the browser's main thread that runs for more than 50ms. The browser cannot respond to user interactions (clicks, taps, keyboard input) while a long task is running — it must finish the current task first. Any interaction that arrives during a long task waits in a queue, accumulating input delay.
Why 50ms is the threshold
Human perception research shows that a response faster than 100ms feels instantaneous. The browser needs approximately 50ms to render a frame at 60fps (16.67ms per frame), so Google established 50ms as the maximum task duration that leaves enough headroom for a frame to be rendered within the 100ms perception window. Tasks longer than 50ms are classified as long tasks in Chrome DevTools.
Common sources of long tasks
- Large JavaScript bundles executing on load. A 500KB JavaScript bundle may take 3–5 seconds to parse and execute on a mid-range mobile device. Any synchronous code in this bundle that runs on the main thread creates long tasks.
- Synchronous third-party scripts. Analytics platforms, tag managers, chat widgets, and ad scripts that execute synchronous JavaScript block the main thread while they run.
- Complex event handlers. onClick or onChange handlers that perform expensive operations — API calls, heavy DOM manipulation, complex calculations — block the main thread during processing.
- Heavy framework updates. React, Vue, and Angular state updates that trigger large component re-renders can create long tasks during DOM reconciliation.
- Synchronous storage access. Reading from localStorage synchronously in event handlers blocks the main thread.
Fixing Input Delay
Input delay is reduced by ensuring that long tasks do not occupy the main thread at the moment a user interacts with the page. The primary strategy is breaking up long tasks so the browser has regular opportunities to process pending inputs.
The scheduler.yield() API (Chrome 115+, now in the HTML spec) explicitly yields control back to the browser, allowing it to process pending user inputs before resuming the current task. This is the most modern and recommended approach to breaking up long tasks:
async function processLargeDataset(items) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield every 50 items to allow browser to process inputs
if (i % 50 === 0) {
await scheduler.yield();
}
}
}
For browsers that do not yet support scheduler.yield(), the traditional fallback is await new Promise(resolve => setTimeout(resolve, 0)) — less precise but widely supported.
JavaScript that runs during page load (initialisation of analytics, feature flags, non-critical UI components) creates long tasks that block the main thread. Deferring this code until after the page becomes interactive reduces input delay for early interactions.
// Instead of running immediately on load:
window.addEventListener('load', () => {
// Defer non-critical work using requestIdleCallback
requestIdleCallback(() => {
initAnalytics();
loadChatWidget();
prefetchNextPageData();
});
});
requestIdleCallback schedules work during browser idle periods, preventing it from blocking user interactions.
Reducing Event Handler Processing Time
Processing time is the time your event handlers take to execute in response to an interaction. Reducing it requires auditing what your event handlers actually do and moving expensive work off the synchronous execution path.
Web Workers run JavaScript on a background thread, completely independent of the main thread. This means complex calculations, data processing, and heavy algorithmic work in event handlers can be moved to a Worker without blocking the UI:
// Main thread
const worker = new Worker('/js/data-processor.js');
button.addEventListener('click', () => {
// Send data to worker instead of processing synchronously
worker.postMessage({ action: 'processData', data: largeDataset });
});
worker.onmessage = (e) => {
updateUI(e.data.result); // Update UI when worker is done
};
Web Workers cannot directly access the DOM but are ideal for computation-heavy tasks: image processing, sorting and filtering large datasets, encryption, and complex calculations.
In React applications, a single state update can trigger re-renders throughout the component tree if components are not properly memoised. This is one of the most common sources of poor INP in React-based sites.
- Use
React.memo()to prevent re-renders of components whose props have not changed - Use
useMemo()to cache expensive calculated values between renders - Use
useCallback()to stabilise function references passed as props - Split large components into smaller ones to limit re-render scope
- Use React 18's concurrent features (
useTransition,startTransition) to mark non-urgent state updates as interruptible
localStorage and sessionStorage are synchronous APIs — reading from them blocks the main thread. In event handlers that fire frequently (scroll listeners, input listeners), synchronous storage access can significantly increase processing time. Use IndexedDB (async) for complex data or cache frequently-needed values in memory (JavaScript variables) rather than reading from storage on every event.
Reducing Presentation Delay
Presentation delay is the time between event handlers finishing and the browser completing the next paint. It is often overlooked because the JavaScript processing is complete — yet the user still sees no visual response. It is caused by expensive style recalculation, layout reflow, and paint operations triggered by the DOM changes from event handlers.
Large DOM trees (Google recommends keeping total DOM nodes under 1,500) increase the time required for style recalculation and layout. When an event handler modifies the DOM, the browser must recalculate styles and layout for potentially large portions of the tree. Reducing DOM complexity directly reduces presentation delay.
Common causes of excessive DOM size: deeply nested CSS frameworks generating many wrapper elements, rendering long lists without virtualisation, keeping off-screen components mounted in the DOM rather than conditionally rendering them.
CSS properties that trigger layout recalculation (width, height, top, left, margin, padding) cause the browser to recalculate positions of affected elements and their siblings. For animations or transitions triggered by user interaction, use only transform and opacity — these are handled by the compositor thread and do not require layout recalculation.
JavaScript Bundle Reduction
The total amount of JavaScript on a page directly correlates with main thread task duration. Every KB of JavaScript must be downloaded, parsed, compiled, and executed — all on the main thread. Reducing JavaScript is therefore one of the highest-leverage INP improvements available.
| Technique | Impact | Implementation |
|---|---|---|
| Code splitting | High | Split bundles by route or feature so only code needed for the current page loads. Webpack, Rollup, Vite all support code splitting via dynamic import() |
| Tree shaking | High | Remove unused code from bundles. Requires ES modules (import/export) rather than CommonJS (require). Enabled by default in Webpack and Rollup with correct configuration |
| Remove unused dependencies | High | Run npx depcheck to identify unused npm packages. Replace heavy libraries with lighter alternatives (e.g. day.js instead of moment.js) |
| Lazy load non-critical components | Medium | Use dynamic import() with React.lazy() or Vue's async components to load non-critical UI components only when needed |
| Minification and compression | Medium | Terser for JS minification; Brotli or gzip compression at the server/CDN level reduces transfer size |
Third-Party Scripts and INP
Third-party scripts are among the most significant contributors to poor INP in real-world sites. A Google analysis of HTTP Archive data found that third-party scripts are responsible for a disproportionate share of long tasks on high-traffic websites. Unlike first-party code, third-party script content is outside your direct control — the focus is on isolation, deferral, and removal.
A facade is a static placeholder that replaces a third-party embed until the user explicitly interacts with it. Chat widgets, video embeds (YouTube, Vimeo), and map embeds are classic candidates. On page load, display a lightweight static image or placeholder. Only load the actual third-party embed when the user clicks on it:
- YouTube: Use the YouTube lite embed pattern — show a thumbnail image, load the YouTube iframe only on click. Saves ~500KB of JavaScript per embed.
- Chat widgets: Show a static "chat" button; load the full widget only when clicked. Most chat platforms support lazy-init APIs.
- Maps: Show a static map screenshot; load the interactive map only on interaction.
Google Tag Manager can fire tags in response to custom events rather than on page load. For analytics, heat mapping, and A/B testing scripts that are not needed for immediate interaction, configure them to fire after the first user interaction (click, scroll) rather than on DOMContentLoaded. This removes their long tasks from the critical page load path.
Measuring and Diagnosing INP
INP is uniquely difficult to measure in lab conditions because it depends on user interactions during a session — synthetic tests that simply load a page cannot capture it. Field data (real user measurements) is essential for understanding your actual INP score.
PageSpeed Insights lab data does not include INP because it requires simulated user interactions. To diagnose INP in Chrome DevTools, you must record a Performance trace and then manually perform the interactions (clicks, taps) that you believe are causing poor INP — then examine the main thread timeline to find long tasks that overlap with or follow those interactions.
Step-by-step INP diagnosis in Chrome DevTools
- Open Chrome DevTools → Performance tab
- Enable "Web Vitals" checkbox in the toolbar
- Click Record, then perform the interaction you suspect is causing high INP (e.g. clicking a button, typing in a field, opening a dropdown)
- Stop recording
- In the Main thread row, look for long red tasks (tasks over 50ms shown in red)
- Find the task that coincides with your interaction — click it to see the call stack
- The call stack shows which functions are responsible for the task duration
- Look for the three phases: input delay (time before event handler starts), processing time (event handler execution), presentation delay (style/layout/paint after handler)
Use the web-vitals library for field INP data
The web-vitals JavaScript library (github.com/GoogleChrome/web-vitals) can be added to your site to collect real user INP measurements, including the interaction type (click/keypress/tap), the affected element, and the sub-part breakdown. Sending this data to your analytics platform gives you real-world INP data stratified by page, device type, and user geography — far more actionable than lab-only measurements.
Authentic Sources
Official INP optimisation guide covering all three sub-parts and improvement strategies.
Official INP definition, how it replaced FID, and threshold values.
How to identify and diagnose long tasks using Chrome DevTools Performance panel.
Techniques for breaking up long tasks including scheduler.yield() and requestIdleCallback.
Official guidance on facade patterns for reducing third-party script impact on INP.
How large DOM trees increase presentation delay and how to reduce DOM size.