Interaction to Next Paint (INP)

Performance & Core Web Vitals — Interaction to Next Paint (INP)
Performance & Core Web Vitals
Chapter 4 · Interaction to Next Paint (INP)

👆 Interaction to Next Paint (INP)

Chapter 3 covered visual stability. This chapter covers responsiveness during actual use — INP, briefly introduced in Chapter 1 as FID's 2024 replacement, now covered with the real measurement mechanics and the main-thread bottleneck behind it.

The Three Sub-Parts of an Interaction

INP measures the full round trip of an interaction, in three stages — a parallel structure to Chapter 2's four-part LCP breakdown:

StageWhat It Covers
Input delayTime before the event handler even starts running
Processing timeHow long the handler itself takes to run
Presentation delayTime until the next frame is actually painted, showing the result

INP reports a high-percentile value (around the 98th percentile, excluding a small number of outliers) across every interaction in the page's session — reinforcing Chapter 1's FID-vs-INP point with the actual measurement behind it: one bad first click no longer defines the score, but neither does a good one.

Why a High Percentile, Not an Average

A single terrible interaction sticks in a user's memory even if most interactions felt fine — averaging would wash that out. A high percentile deliberately keeps the worst realistic experience visible.

Every Interaction, Not Just the First

Unlike FID, a page that's fast on click one but sluggish on click fifteen will show that in its INP score — nothing before this metric tracked that at all.

🧵 Why the Main Thread Is the Bottleneck

JavaScript execution, style recalculation, layout, and paint all compete for the same single main thread. If a long JS task is already running when a user clicks, the click's event handler can't even start until that task finishes — directly inflating input delay.

Long Tasks

Any task occupying the main thread for more than 50ms is considered a "long task" — the most common root cause of poor INP, detectable via the Long Tasks API.

Common Causes

Heavy synchronous JS, large third-party scripts (analytics, ads, chat widgets) running arbitrary code on the same thread, framework re-renders touching too much of the DOM, and expensive work done directly inline inside event handlers.

A Direct Link to Chapter 7

Expensive DOM queries or reflows running on every scroll event is exactly the debounce/throttle problem from JS Advanced, applied here at the whole-page level — Chapter 7 covers this in depth.

Breaking Up Long Tasks

The fix is yielding to the main thread periodically during expensive work, so pending input and paint can be processed between chunks:

Blocking vs Yielding

// Blocking — one long task, the click handler can't run until this finishes function processAll(items) { for (const item of items) { expensiveWork(item); } } // Yielding — breaks the same work into chunks the browser can interrupt function processInChunks(items, i = 0) { const end = Math.min(i + 50, items.length); for (; i < end; i++) { expensiveWork(items[i]); } if (i < items.length) { setTimeout(() => processInChunks(items, i), 0); // yield, then resume } }

setTimeout(fn, 0) is the classic yielding technique; requestIdleCallback suits lower-priority background work, and the newer scheduler.yield() offers a cleaner, purpose-built API for the same idea.

Felt Experience

Blocking

A click during the loop sits queued for hundreds of milliseconds — visible, frustrating lag.

Yielding

The same total work completes, but the browser can slip the click's handler in between chunks — it feels instant.

💻 Coding Challenges

Challenge 1: Identify the Long Task

A button click triggers a function that loops over 10,000 array items doing synchronous work, taking 300ms total, before updating the DOM. Explain which INP sub-part this most directly inflates, and why.

Goal: Practice mapping a concrete code pattern onto the three-part INP breakdown.

→ Solution

Challenge 2: Chunk a Long-Running Function

Rewrite a function that synchronously processes 5,000 items in one pass into a chunked version using the pattern from this chapter, processing 100 items per chunk.

Goal: Practice applying the yielding pattern to a different chunk size and workload.

→ Solution

Challenge 3: Explain Why Percentile Beats Average Here

A page has 100 interactions in a session: 95 take 50ms and 5 take 800ms. Explain what the average would suggest about the page's responsiveness versus what a high-percentile INP score would show, and why the high-percentile view is more useful.

Goal: Practice reasoning about why INP's percentile design choice matters in a concrete case.

→ Solution

⚠️ Gotcha: Assuming INP Is Only About Your Own Code

Third-party scripts — analytics, ad networks, chat widgets, tag managers — run on the exact same main thread as first-party code, and are extremely common, often dominant, contributors to poor INP. It's easy to profile and optimize a team's own event handlers while a marketing team's tag manager script quietly runs a 400ms long task on every page. Auditing INP means auditing every script actually running on the page, not just the ones a given team wrote — third-party scripts are often added by other teams without engineering ever seeing their performance cost.

🎯 What's Next

With all three Core Web Vitals covered in depth, the next chapter turns to a concrete, high-impact fix for several of them at once: Image Optimization — modern formats, responsive images, and lazy loading.