Google officially retired First Input Delay (FID) on March 12, 2024, replacing it with Interaction to Next Paint (INP) as a Core Web Vital metric. This shift represents the most significant change to the Core Web Vitals initiative since its inception, fundamentally altering how developers and site owners measure and optimize for interactivity. Understanding the nuances of this transition is critical for maintaining search visibility and delivering a genuinely responsive user experience And it works..
Why Google Replaced FID with INP
For years, FID served as the primary proxy for interactivity. It measured the time from when a user first interacts with a page—clicking a link, tapping a button, or using a custom JavaScript-powered control—to the time when the browser is actually able to begin processing event handlers in response to that interaction That's the part that actually makes a difference..
On the flip side, FID had a glaring limitation: it only measured the first interaction. So naturally, it completely ignored the responsiveness of the page throughout the entire user session. A site could pass FID with flying colors while feeling sluggish and broken during subsequent clicks, scrolls, or keystrokes.
People argue about this. Here's where I land on it It's one of those things that adds up..
INP solves this by assessing all interactions. It observes the latency of every click, tap, and keyboard interaction throughout the full lifecycle of a page visit. The final INP value is the single worst interaction latency (or a high percentile for sites with many interactions), providing a holistic view of responsiveness rather than a snapshot of the initial load Simple as that..
Understanding Interaction to Next Paint (INP)
At its core, INP measures the time between a user action and the next visual update—the "next paint." An interaction consists of three distinct phases, and INP captures the total duration of all three:
- Input Delay: The time between the user interaction and when the event callbacks begin to execute. This is essentially what FID measured in isolation.
- Processing Time: The time it takes for the event handlers (JavaScript) to run and complete their work.
- Presentation Delay: The time between the completion of the event handlers and the browser painting the next frame to reflect the visual result.
INP = Input Delay + Processing Time + Presentation Delay
This formula highlights why optimizing for INP requires a broader strategy than FID. Reducing Input Delay alone (often achieved by breaking up Long Tasks) is insufficient if Processing Time is bloated by heavy logic or if Presentation Delay is high due to forced synchronous layouts or complex rendering work Simple, but easy to overlook. Took long enough..
What Counts as an Interaction?
INP considers three specific interaction types:
- Mouse clicks on interactive elements. Practically speaking, * Touchscreen taps on interactive elements. * Key presses on physical or on-screen keyboards.
Crucially, hovering and scrolling are not measured by INP (scrolling is measured by a separate metric, Scroll Performance). On the flip side, a click on a non-interactive element (like a div without a click handler) that triggers a style change or focus shift can count.
INP Thresholds: Good, Needs Improvement, Poor
To pass the Core Web Vitals assessment, your site's 75th percentile INP must meet the "Good" threshold across both mobile and desktop.
| Rating | Threshold (Milliseconds) |
|---|---|
| Good | ≤ 200 ms |
| Needs Improvement | > 200 ms and ≤ 500 ms |
| Poor | > 500 ms |
A "Good" INP of 200ms aligns with the RAIL model's response goal, ensuring the user perceives the interaction as instantaneous. Anything above 500ms creates a noticeable lag that frustrates users and increases bounce rates.
The Technical Differences: FID vs. INP
The transition from FID to INP is not merely a renaming exercise; it is a paradigm shift in measurement methodology.
| Feature | First Input Delay (FID) | Interaction to Next Paint (INP) |
|---|---|---|
| Scope | First interaction only | All interactions (worst-case/percentile) |
| Phases Measured | Input Delay only | Input Delay + Processing + Presentation |
| Event Types | Click, Keydown, Mousedown, Pointerdown | Click, Keydown, Tap (Pointerup) |
| Main Thread Focus | Blocking time before first event | Total main thread congestion during interaction |
| Field Data Requirement | Requires real user interaction | Requires real user interaction |
| Lab Measurability | Not measurable in Lab (Lighthouse uses TBT) | Measurable in Lab via Total Blocking Time (TBT) proxy and user flows |
Because INP measures the entire interaction lifecycle, it correlates much more strongly with Total Blocking Time (TBT) in lab tools like Lighthouse. If you optimize TBT, you generally improve INP.
Common Causes of Poor INP
Diagnosing a high INP requires identifying which interaction is the culprit and which phase is causing the delay.
1. Heavy JavaScript Execution (Processing Time)
This is the most common offender. Large bundles, expensive framework hydration, complex state management updates (e.g., massive Redux/Vuex/Pinia stores recalculating), or inefficient algorithms running on the main thread block the browser from painting.
- Symptom: Long Tasks (> 50ms) visible in the Performance Profiler during the interaction.
2. Layout Thrashing / Forced Synchronous Layouts (Presentation Delay)
Reading layout properties (offsetHeight, getBoundingClientRect, scrollTop) immediately after writing to the DOM (changing styles, classes, or content) forces the browser to calculate layout synchronously, mid-script. This destroys the browser's optimized rendering pipeline And it works..
- Symptom: "Recalculate Style" and "Layout" tasks appearing immediately after script execution in the flame chart.
3. Large DOM Size
A massive DOM tree increases the cost of style recalculation, layout, and painting. Every interaction that triggers a style change forces the browser to traverse a larger tree The details matter here..
- Symptom: High "Recalculate Style" and "Paint" times proportional to DOM node count.
4. Third-Party Scripts
Ads, analytics, chat widgets, and A/B testing tools often run heavy logic on the main thread, competing with your first-party code for the single thread of execution Small thing, real impact..
- Symptom: Long Tasks attributed to third-party domains in the Performance panel.
5. Input Delay (Main Thread Congestion)
While less common as the sole cause of a poor INP compared to Processing Time, a congested main thread during startup (heavy hydration) can delay the first few interactions significantly.
Strategies to Optimize INP
Optimizing INP is an iterative process of measurement, identification, and refactoring.
1. Identify the Slow Interaction
You cannot fix what you cannot measure. Use the Web Vitals JavaScript library or the Chrome User Experience Report (CrUX) via PageSpeed Insights or Search Console to find the specific element causing the 75th percentile INP Simple, but easy to overlook. And it works..
- In DevTools Performance Panel: Record a session, interact with the suspect element, and analyze the "Interactions" track (introduced in Chrome 118+). It visually breaks down Input Delay, Processing, and Presentation.
2. Yield to the Main Thread (Reduce Processing Time)
Break up long-running JavaScript tasks so the browser can paint between chunks It's one of those things that adds up..
scheduler.yield()(Scheduler API): The modern, explicit way to yield control back to the browser. It returns a promise that resolves in a new task, allowing painting to occur.setTimeout(fn, 0)/postMessage: Legacy yielding techniques. Less ergonomic thanscheduler.yield()but widely supported.- **Web
Workers: Move heavy computation (data parsing, sorting, filtering, encryption) off the main thread entirely. While Web Workers cannot access the DOM directly, libraries like Comlink make the RPC (Remote Procedure Call) ergonomics nearly seamless, allowing you to treat worker functions as async local functions.
3. Eliminate Layout Thrashing (Reduce Presentation Delay)
Batch your DOM reads and writes strictly. Never interleave them.
- Read all, then write all: Calculate all required dimensions (
getBoundingClientRect,offsetWidth) first, store them in variables, then apply all DOM mutations (style changes, class toggles,innerHTML). - CSS Containment: Use
content-visibility: autoon off-screen sections (like long lists or below-the-fold articles). This tells the browser to skip layout and paint for that subtree until it approaches the viewport. contain: layout style paint: Apply this to isolated widgets (chat windows, complex cards) to prevent style/layout changes inside from bubbling up and invalidating the whole page layout.
4. Virtualize Large Lists
If your INP culprit is a list with hundreds of rows (e.g., a data grid, infinite scroll feed), virtualization is non-negotiable. Render only the visible items (plus a small buffer). Libraries like TanStack Virtual, react-window, or lit-virtualizer reduce the DOM node count from thousands to dozens, drastically lowering Recalculate Style and Layout costs during interactions like scrolling, filtering, or row expansion.
5. Optimize Event Handlers & Event Delegation
- Passive Listeners: Always use
{ passive: true }fortouchstart,wheel, andscrolllisteners where you don't callpreventDefault(). This allows the browser to start scrolling/compositing immediately without waiting for your JS to finish. - Event Delegation: Attach a single listener to a parent container instead of individual listeners to hundreds of children (e.g., a table row
clickhandler). This reduces memory pressure and attachment overhead during hydration/rendering. - Debounce/Throttle High-Frequency Events: For
resize,scroll, ormousemovehandlers that trigger layout or state updates, wrap the callback inrequestAnimationFrameor a lodashthrottleto align work with the browser's frame rate.
6. Tame Third-Party Scripts
You cannot optimize what you don't control, but you can control when and how it runs.
- Lazy Load with
partytown: Move third-party scripts (Google Analytics, Meta Pixel, chat widgets) into a Web Worker using Partytown. It proxies DOM API calls from the worker to the main thread, keeping the main thread free for user interactions. - Facade Pattern: Replace heavy third-party embeds (YouTube players, Maps, Chat widgets) with a lightweight static placeholder (an image + play button). Load the real embed only on user click/interaction.
- Resource Hints: Use
<link rel="preconnect" href="https://third-party.domain">anddns-prefetchfor critical third-party origins to reduce connection latency when they do load.
7. Framework-Specific Hydration Strategies
If you use a hydration-heavy framework (Next.js, Nuxt, Astro, Remix), the "Input Delay" during startup is often the bottleneck.
- Partial Hydration / Islands Architecture (Astro, Fresh, Qwik): Only hydrate interactive components; ship static HTML for the rest.
- Progressive/Selective Hydration (React 18+): Wrap heavy subtrees in
<Suspense>boundaries. React will hydrate the interactive parts first (prioritizing the component the user clicked) and defer the rest. - Resumability (Qwik, Wiz/Google Search): Serialize framework state into HTML so the client requires zero JavaScript execution to become interactive. The event listener is global and lazy-loads only the specific handler code on demand.
8. CSS will-change & transform for Animations
If an interaction triggers an animation (modals, drawers, tooltips), ensure it runs on the Compositor Thread, not the Main Thread And it works..
- Animate only
transformandopacity. - Promote the element to its own compositor layer before the interaction:
will-change: transform, opacity(remove it after animation viatransitionendto save memory). - Avoid animating
width,height,top,left,margin, orpadding—these trigger Layout on every frame.
The Optimization Loop
The Optimization Loop
Performance work is never a one‑off sprint; it’s a feedback‑driven cycle that keeps your app fast as it grows. The loop below ties together measurement, analysis, experimentation, and iteration so you can turn vague “it feels slow” feelings into concrete, data‑backed improvements.
The official docs gloss over this. That's a mistake.
1. Measure
- Baseline metrics – Capture Core Web Vitals, navigation timing, interaction latency, and memory usage in CI (e.g.,
lighthouse-ci,web-vitals-instrumental). Store results in a dashboard (Google Data Studio, Grafana, or a custom page) to spot trends. - Targeted profiling – Use Chrome DevTools → Performance panel for a typical user journey, or
perfettofor mobile. Export the trace and look for long tasks, main‑thread blocking, and inefficient paints. - User‑centric telemetry – If you have a real‑time analytics stack, instrument custom metrics like “time to interactive”, “first input delay”, and “layout thrashing count”. This tells you how the actual users experience the app.
2. Analyze
- Identify bottlenecks – Correlate high‑impact entries (e.g., a 120 ms paint that blocks the frame) with the code that generated them. Stack traces from
performance.getEntriesByType('measure')or custom performance marks help pinpoint the source. - Prioritize by impact & effort – Rank issues using an impact/effort matrix. A 30 ms layout thrash caused by a popular component deserves immediate attention, while a niche animation that only runs on a hidden page can wait.
- Benchmark against budgets – Define numeric budgets (e.g., “first paint ≤ 1.5 s on 4G”, “total blocking time ≤ 300 ms”). Anything above the threshold becomes a concrete problem, not a subjective feeling.
3. Hypothesize & Plan
- Form a hypothesis – “Removing the
clickhandler from the TableRow component will cut hydration time by ~15 ms and reduce memory pressure by 2 MB.” - Choose the solution – Decide between a code refactor, a library swap (e.g.,
partytownfor third‑party scripts), or a UI pattern change (placeholder + lazy load). - Estimate impact – Use a quick prototype or a staging environment to gauge the effect before committing to production code.
4. Implement & Ship
- Commit to source control – Tag the change with a clear, descriptive message (
perf: reduce hydration overhead on TableRow). - Versioning & roll‑back – If you’re using feature flags, wrap the new behavior behind a flag so you can quickly revert if an unexpected regression appears.
- Documentation – Add a short note in your performance‑guidelines doc (e.g., “avoid per‑row click handlers during hydration”) so future engineers understand the rationale.
5. Validate
- Automated checks – Integrate Lighthouse or a custom performance test into your CI pipeline. Fail the build if a metric breaches the budget.
- User‑facing monitoring – Let the performance instrumentation run in production for a few days. Look for any spikes or regressions that only appear under real load.
- A/B testing – For high‑visibility changes (e.g., switching a heavy embed to a lazy placeholder), run a lightweight A/B test to confirm the UX improvement matches the performance gain.
6. Iterate
- Refine – If the improvement is partial, dig deeper (e.g., the TableRow still shows a brief jank). Perhaps the event listener is being attached on the server side; move it to a client‑only import.
- Capture learning – Update your performance‑guideline checklist with the new rule (
perf: use partytown for analytics). This creates a feedback loop that reduces repeat work.