Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 044 / 100

Interaction to Next Paint: practical tips and trade-offs

INP replaced FID in 2024. It measures real responsiveness.

Expona Admin7 min read

In 2024, Google replaced First Input Delay (FID) with a new Core Web Vital called Interaction to Next Paint (INP), and if you're still optimizing for the old metric, you're already behind. INP measures the real-world responsiveness of your site by capturing the delay between when a user interacts with your page and when the browser can actually paint the next visual update. Unlike FID, which only sampled the first interaction, INP looks at all interactions, giving you a far more accurate picture of whether your site feels snappy or sluggish.

What Interaction to Next Paint Actually Measures

Let me be direct: INP is about user perception. When someone clicks a button, taps a dropdown, or types in a search field, they expect immediate visual feedback. Interaction to Next Paint quantifies exactly how long they have to wait.

The flow is simple in theory but complex in practice:

  1. User performs an interaction (click, tap, keyboard input)
  2. Browser receives the event
  3. JavaScript runs to handle the event
  4. Browser paints the new visual state

INP measures the time from step 1 to step 4. Google's target is under 200 milliseconds. Anything above 500ms is considered "poor" and will harm your Lighthouse score and SEO rankings.

Here's what makes this different from FID: FID only looked at the first interaction of a session. INP evaluates all interactions on a page. So if your checkout button is fast but your product filter is slow, INP catches it. This is why many sites thought they were fine with FID but suddenly see poor INP scores once they start measuring it properly.

Real example from my work: we built an e-commerce product listing page that performed beautifully on initial load. FID was great. But when users clicked the "Sort" dropdown, it would lag for 800ms while the page re-sorted and re-rendered 50 products. INP exposed this immediately.

The Main Culprit: Long JavaScript Tasks Blocking the Main Thread

JavaScript runs on a single thread in the browser. When you have a task that takes longer than ~50ms, it blocks everything else—event handlers, repaints, user interactions. This is the primary reason INP fails.

Common offenders I see regularly:

  • Large JSON parsing: Processing a 5MB API response synchronously
  • DOM manipulation in loops: Updating 1,000 elements one by one
  • Heavy computations: Image processing, data sorting, filtering without chunking
  • Unoptimized React renders: A parent component re-rendering 100 child components unnecessarily
  • Synchronous file operations: Reading files or databases on the main thread

Let me show you a real scenario I debugged at a travel booking platform. The interaction was clicking "Apply Filters" on a search results page with 200 flight results:

html
// BAD: This blocks the main thread for ~2 seconds
function applyFilters(flights, filters) {
  const filtered = flights.filter(flight => {
    return flight.price <= filters.maxPrice &&
           flight.duration <= filters.maxDuration &&
           flight.airline === filters.airline &&
           checkAvailability(flight) &&
           validateRules(flight);
  });
  
  const sorted = filtered.sort((a, b) => a.price - b.price);
  const grouped = groupByAirline(sorted);
  
  renderResults(grouped);
}

// GOOD: Break work into chunks
function applyFiltersOptimized(flights, filters) {
  let filtered = [];
  let index = 0;
  
  function processChunk() {
    const chunkSize = 20;
    const end = Math.min(index + chunkSize, flights.length);
    
    for (let i = index; i < end; i++) {
      if (matchesFilters(flights[i], filters)) {
        filtered.push(flights[i]);
      }
    }
    
    index = end;
    
    if (index < flights.length) {
      requestIdleCallback(processChunk);
    } else {
      const sorted = filtered.sort((a, b) => a.price - b.price);
      renderResults(sorted);
    }
  }
  
  processChunk();
}

The difference? The bad version blocks for 2 seconds. The optimized version yields to the browser every 20 items, allowing user interactions to be handled while filtering still happens in the background. INP improves dramatically.

Fixes: Practical Strategies to Reduce INP

Break Up Long Tasks

Use requestIdleCallback and setTimeout(..., 0) to chunk work into smaller pieces. The browser has a "time budget" of about 50ms per frame (at 60fps). Respect it.

If you're using React, this becomes critical during hydration. A Next.js app with large component trees can block user interactions for seconds during hydration if not handled carefully.

Defer Non-Critical Scripts

Mark scripts as defer or async to keep them off the critical path:

html
<!-- Blocks rendering until downloaded and executed -->
<script src="analytics.js"></script>

<!-- Downloads in parallel, executes after HTML parsing -->
<script src="analytics.js" defer></script>

<!-- Downloads in parallel, executes immediately when ready -->
<script src="analytics.js" async></script>

For analytics and monitoring tools, defer is usually safe. For critical functionality, you need blocking scripts, but keep them small.

Reduce Hydration Cost in Next.js / SSR

Server-side rendering is great for FCP and LCP, but if your JS bundle is huge, hydration will destroy INP. Users see content but can't interact with it.

Strategies I've used:

  • Progressive hydration: Only hydrate interactive regions first. Defer hydration of content-only areas.
  • Selective hydration: Skip hydration for static components. Use suppressHydrationWarning carefully.
  • Code splitting: Split your bundle by route and component. Load only what's needed.
  • Preload critical chunks: Use <link rel="preload"> for chunks needed for initial interactions.

In a SaaS CRM I worked on, we were hydrating an entire page tree including unused tabs and panels. Simply deferring hydration of inactive tabs cut INP from 850ms to 120ms.

Third-Party Scripts: The Silent INP Killers

Here's where I see the biggest opportunities for improvement: third-party scripts. A chat widget, analytics tool, tag manager, or ad network can absolutely tank your INP, and many developers don't realize it until measurement tools reveal it.

I've seen:

  • A live chat widget that injected 400KB of unminified JavaScript
  • A tag manager that parsed and executed Google Tag Manager rules synchronously on every interaction
  • An analytics provider that made blocking network requests on every click
  • An A/B testing tool that re-rendered the entire page to show variants

How to handle third-party scripts safely:

html
<!-- Load chat widget asynchronously and lazily -->
<script>
  window.addEventListener('load', () => {
    if (document.querySelector('[data-chat-eligible]')) {
      const script = document.createElement('script');
      script.src = 'https://chat-vendor.com/widget.js';
      script.async = true;
      document.body.appendChild(script);
    }
  });
</script>

<!-- Or use a service worker to isolate third-party work -->
<!-- This keeps blocking scripts off the main thread -->

Better yet: sandbox third-party scripts in an iframe or Web Worker. This prevents them from blocking your main thread entirely. It's more effort upfront but worth it for critical interactions.

At a ride-hailing platform, we moved Segment (analytics) and Intercom (chat) to load after the first interaction. INP improved 40%. Users didn't notice any delay in those tools initializing because it happened while they were reading the page.

Common Mistakes

  • Focusing only on FCP/LCP: You can have a fast first paint but terrible INP if your JavaScript is bloated. They're not correlated.
  • Not using Chrome DevTools to measure: The Performance tab shows you exactly where the main thread is blocked. Use it. Set a throttle and reproduce the interaction.
  • Ignoring real-user monitoring (RUM): Lab data (Lighthouse) and field data (Web Vitals API) can differ significantly. Use something like web-vitals library to capture real INP values from your users.
  • Assuming mobile is fine if desktop is fast: Mobile has less CPU power. An interaction that's 100ms on desktop might be 600ms on a mid-range Android. Test on real devices.
  • Only testing with your own JavaScript: Third-party scripts often cause worse INP issues than your own code. Audit them.
  • Deferring everything: Some scripts need to run early. Payment forms, security checks, critical feature flags. Don't defer blindly; defer strategically.

Key Takeaways

  • INP replaced FID in 2024 and measures the delay from user interaction to the next visual paint. The target is under 200ms.
  • Long JavaScript tasks blocking the main thread are the primary cause. Break them into chunks using requestIdleCallback and yielding patterns.
  • Defer non-critical scripts and use async where appropriate. This keeps the critical path clear for interaction handlers.
  • Reduce hydration cost in SSR/Next.js by progressively or selectively hydrating only what users need to interact with immediately.
  • Third-party scripts (chat, analytics, tag managers, ads) frequently cause INP failures. Lazy-load them, defer them, or sandbox them in iframes.
  • Measure with real user data, not just Lighthouse. Use the web-vitals library to capture INP from your actual traffic.
  • Test on mid-range mobile devices. Desktop performance is often misleading.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles