Technical

Interaction to Next Paint (INP)

A Core Web Vitals metric that measures the latency of all user interactions throughout a page visit, reporting the longest duration from user input to visual feedback.

Quick Answer

  • What it is: A Core Web Vitals metric that measures the latency of all user interactions throughout a page visit, reporting the longest duration from user input to visual feedback.
  • Why it matters: INP provides a comprehensive view of page responsiveness, replacing FID as a Core Web Vital in March 2024.
  • How to check or improve: Optimize JavaScript execution, minimize main thread work, and reduce input delay to achieve INP under 200ms.

When you'd use this

INP provides a comprehensive view of page responsiveness, replacing FID as a Core Web Vital in March 2024.

Example scenario

Hypothetical scenario (not a real company)

A team might use Interaction to Next Paint (INP) when Optimize JavaScript execution, minimize main thread work, and reduce input delay to achieve INP under 200ms.

Common mistakes

  • Confusing Interaction to Next Paint (INP) with Core Web Vitals: A set of three specific metrics that Google uses to measure user experience on websites: loading performance (LCP), interactivity (INP), and visual stability (CLS). Learn how to measure and improve your Core Web Vitals scores.
  • Confusing Interaction to Next Paint (INP) with First Input Delay (FID): A Core Web Vitals metric that measures the time from when a user first interacts with a page to the time when the browser is able to begin processing event handlers in response.
  • Confusing Interaction to Next Paint (INP) with Total Blocking Time (TBT): A lab metric that measures the total time the main thread is blocked by long tasks, preventing user input responsiveness between First Contentful Paint and Time to Interactive.

How to measure or implement

  • Optimize JavaScript execution, minimize main thread work, and reduce input delay to achieve INP under 200ms

Analyze your INP performance

Start here
Updated Jan 20, 2026·5 min read

Interaction to Next Paint (INP) is the newest Core Web Vitals metric, set to replace First Input Delay (FID) in March 2024. While FID only measures the first interaction, INP observes all interactions throughout a user's visit and reports the worst latency. This comprehensive approach better represents the actual responsiveness users experience across their entire session.

What is Interaction to Next Paint?

INP measures the time from when a user initiates an interaction (click, tap, or key press) until the browser paints the next frame showing visual feedback. It captures the full interaction lifecycle:

  1. Input delay: Time until event handlers start
  2. Processing time: JavaScript execution duration
  3. Presentation delay: Time to compute layout and paint

The metric reports the worst interaction latency (with statistical outlier removal), providing a realistic assessment of a page's responsiveness throughout its lifecycle.

Google's INP thresholds are:

  • Good: 200ms or less
  • Needs Improvement: 200ms to 500ms
  • Poor: More than 500ms

These thresholds ensure that interactions feel responsive across the 75th percentile of page loads.

Why INP Matters: The Evolution from FID

Comprehensive Responsiveness Measurement

FID's limitation was measuring only the first interaction's input delay, missing:

  • Subsequent interactions that might be slower
  • Processing time after input delay
  • Visual feedback delay

INP addresses these gaps by:

  • Measuring all user interactions
  • Including full interaction duration
  • Capturing presentation delay
  • Representing ongoing responsiveness

Google's research shows that INP correlates better with user-perceived responsiveness than FID. Pages with good INP scores see:

  • 28% higher user satisfaction scores
  • 23% longer session durations
  • 19% better task completion rates

Real-World Impact

Early adopters optimizing for INP report significant improvements:

RedBus improved INP by 72% and saw:

  • 7% increase in sales
  • Significantly improved user experience scores

The Economic Times reduced INP from 1,000ms to 200ms:

  • 50% reduction in bounce rate
  • 43% increase in page views

Tokopedia achieved INP under 200ms:

  • 55% better interaction metrics
  • 23% increase in add-to-cart actions

SEO Implications

As INP becomes a Core Web Vital in March 2024, it will directly impact:

  • Search rankings (especially mobile)
  • Featured snippet eligibility
  • Top Stories inclusion for news sites
  • Google Discover visibility

How INP Works: Technical Architecture

Interaction Types and Measurement

INP measures discrete interactions:

Measured Interactions:

  • Mouse clicks
  • Touchscreen taps
  • Keyboard key presses (physical or onscreen)

Not Measured:

  • Hovering (mouse or stylus)
  • Scrolling (considered continuous)
  • Zooming (continuous action)

The Interaction Lifecycle

Each interaction follows this sequence:

User Input → Input Delay → Processing → Presentation Delay → Next Paint
    ↑            ↓             ↓                ↓               ↓
  Start      Event Handler   JS Execution    Layout/Style    Visual Update

Input Delay Phase:

  • Waiting for main thread availability
  • Affected by other JavaScript execution
  • Influenced by long tasks

Processing Phase:

  • Event handler execution
  • State updates
  • DOM manipulation
  • Network requests initiated

Presentation Delay Phase:

  • Style recalculation
  • Layout computation
  • Paint preparation
  • Compositor work

INP Calculation Algorithm

INP uses sophisticated calculation to avoid outliers:

  1. Collect all interactions during page lifetime
  2. Group related events (e.g., pointerdown, pointerup, click)
  3. Calculate duration for each interaction group
  4. Select representative value:
    • For <50 interactions: Worst interaction
    • For ≥50 interactions: 98th percentile
  5. Report as INP score

This approach ensures one extremely slow interaction doesn't unfairly penalize otherwise responsive pages.

Browser APIs and Implementation

INP relies on the Event Timing API:

// Observe interaction performance
new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    if (entry.interactionId) {
      // This is an interaction
      const inp = entry.processingEnd - entry.startTime
      console.log(`Interaction duration: ${inp}ms`)
      console.log(`Type: ${entry.name}`)
      console.log(`Target: ${entry.target}`)
    }
  }
}).observe({ type: "event", buffered: true })

Best Practices for Optimizing INP

1. Minimize JavaScript Execution Time

Break Up Long Tasks

// Bad: Blocking task
function processAllItems(items) {
  items.forEach(item => {
    complexProcessing(item) // 10ms per item
  })
}

// Good: Yielding periodically
async function processItemsWithYield(items) {
  for (const item of items) {
    complexProcessing(item)
    // Yield to browser every 50ms
    if (performance.now() % 50 < 10) {
      await scheduler.yield()
    }
  }
}

// Better: Using scheduler.postTask
async function processWithPriority(items) {
  for (const item of items) {
    await scheduler.postTask(
      () => {
        complexProcessing(item)
      },
      { priority: "background" }
    )
  }
}

Debounce Event Handlers

// Bad: Executing on every input
input.addEventListener("input", e => {
  performExpensiveSearch(e.target.value)
})

// Good: Debounced execution
const debouncedSearch = debounce(value => {
  performExpensiveSearch(value)
}, 300)

input.addEventListener("input", e => {
  debouncedSearch(e.target.value)
})

2. Optimize Event Handlers

Use Event Delegation

// Bad: Multiple listeners
document.querySelectorAll(".button").forEach(btn => {
  btn.addEventListener("click", handleClick)
})

// Good: Single delegated listener
document.addEventListener("click", e => {
  if (e.target.closest(".button")) {
    handleClick(e)
  }
})

Passive Event Listeners

// Enable browser optimizations
document.addEventListener("touchstart", handler, { passive: true })
document.addEventListener("wheel", handler, { passive: true })

Avoid Forced Synchronous Layouts

// Bad: Read-write-read pattern
function moveElements(elements) {
  elements.forEach(el => {
    const left = el.offsetLeft // Forces layout
    el.style.left = left + 10 + "px" // Invalidates layout
  })
}

// Good: Batch reads and writes
function moveElementsOptimized(elements) {
  // Batch reads
  const positions = elements.map(el => el.offsetLeft)

  // Batch writes
  elements.forEach((el, i) => {
    el.style.left = positions[i] + 10 + "px"
  })
}

3. Reduce Input Delay

Minimize Main Thread Work

// Move heavy computation to Web Worker
const worker = new Worker("processor.js")

button.addEventListener("click", async () => {
  // Show immediate feedback
  button.classList.add("loading")

  // Offload heavy work
  worker.postMessage({ cmd: "process", data })

  worker.onmessage = e => {
    updateUI(e.data)
    button.classList.remove("loading")
  }
})

Use requestIdleCallback for Non-Critical Work

// Schedule non-critical updates
requestIdleCallback(
  () => {
    updateAnalytics()
    preloadNextPageResources()
    cleanupOldData()
  },
  { timeout: 2000 }
)

4. Optimize Rendering Performance

Use CSS Containment

/* Isolate layout calculations */
.component {
  contain: layout style paint;
}

/* Strict containment for independent widgets */
.widget {
  contain: strict;
  content-visibility: auto;
}

Optimize Animations

/* Bad: Animating layout properties */
.menu {
  transition: height 0.3s;
}

/* Good: Transform and opacity only */
.menu {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.3s;
}

.menu.open {
  transform: scaleY(1);
}

Use will-change Sparingly

/* Prepare for animation */
.modal {
  will-change: transform, opacity;
}

/* Remove after animation */
.modal.settled {
  will-change: auto;
}

5. Implement Loading States

Optimistic UI Updates

// Immediate visual feedback
async function likePost(postId) {
  // Optimistic update
  const likeButton = document.querySelector(`#like-${postId}`)
  likeButton.classList.add("liked")
  likeButton.disabled = true

  try {
    await api.likePost(postId)
  } catch (error) {
    // Revert on failure
    likeButton.classList.remove("liked")
    likeButton.disabled = false
    showError("Failed to like post")
  }
}

Progressive Enhancement

<!-- Functional without JavaScript -->
<form action="/search" method="GET">
  <input name="q" type="search" />
  <button type="submit">Search</button>
</form>

<script type="module">
  // Enhance when JavaScript loads
  import { EnhancedSearch } from "./search.js"
  new EnhancedSearch(document.querySelector("form"))
</script>

Common INP Issues and Solutions

Issue 1: Third-Party Scripts

Problem: Analytics, ads, and widgets blocking the main thread

Solutions:

// Load third-party scripts after interaction
let scriptsLoaded = false

function loadThirdPartyScripts() {
  if (scriptsLoaded) return

  // Load analytics
  const script = document.createElement("script")
  script.src = "https://analytics.example.com/script.js"
  script.async = true
  document.body.appendChild(script)

  scriptsLoaded = true
}

// Load on first interaction
;["click", "scroll", "touchstart"].forEach(event => {
  document.addEventListener(event, loadThirdPartyScripts, {
    once: true,
    passive: true
  })
})

Issue 2: React Re-rendering

Problem: Unnecessary re-renders blocking interactions

Solutions:

// Use React.memo for expensive components
const ExpensiveComponent = React.memo(
  ({ data }) => {
    return <ComplexVisualization data={data} />
  },
  (prevProps, nextProps) => {
    // Custom comparison
    return prevProps.data.id === nextProps.data.id
  }
)

// Use useMemo for expensive calculations
const ProcessedData = () => {
  const data = useSelector(state => state.data)

  const processed = useMemo(() => {
    return expensiveProcessing(data)
  }, [data.id]) // Only recalculate when ID changes

  return <Display data={processed} />
}

// Use useTransition for non-urgent updates
const SearchResults = () => {
  const [query, setQuery] = useState("")
  const [isPending, startTransition] = useTransition()

  const handleSearch = e => {
    // Urgent: Update input immediately
    setQuery(e.target.value)

    // Non-urgent: Update results
    startTransition(() => {
      updateSearchResults(e.target.value)
    })
  }

  return (
    <>
      <input onChange={handleSearch} value={query} />
      {isPending && <Spinner />}
      <Results />
    </>
  )
}

Issue 3: Large DOM Updates

Problem: Updating large lists or tables blocks the main thread

Solutions:

// Virtual scrolling for large lists
import { VirtualList } from "@tanstack/react-virtual"

const LargeList = ({ items }) => {
  const virtualizer = useVirtual({
    size: items.length,
    parentRef,
    estimateSize: useCallback(() => 50, [])
  })

  return (
    <div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
      <div style={{ height: virtualizer.totalSize }}>
        {virtualizer.virtualItems.map(virtualRow => (
          <div
            key={virtualRow.index}
            style={{
              position: "absolute",
              top: 0,
              transform: `translateY(${virtualRow.start}px)`
            }}
          >
            <Item data={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

Tools for Measuring and Debugging INP

Chrome DevTools

Performance Panel:

  • Record interactions
  • Identify long tasks
  • Analyze interaction breakdown
  • View main thread activity

Performance Monitor:

  • Real-time INP tracking
  • CPU usage monitoring
  • Layout shifts per second

Web Vitals Library

import { onINP } from "web-vitals"

onINP(metric => {
  console.log("INP:", metric.value)
  console.log("Rating:", metric.rating) // 'good', 'needs-improvement', 'poor'

  // Get attribution data
  const attribution = metric.attribution
  console.log("Event type:", attribution.eventType)
  console.log("Event target:", attribution.eventTarget)
  console.log("Input delay:", attribution.inputDelay)
  console.log("Processing duration:", attribution.processingDuration)
  console.log("Presentation delay:", attribution.presentationDelay)
})

Field Data Collection

// Custom RUM implementation
class INPMonitor {
  constructor() {
    this.interactions = []
    this.observer = new PerformanceObserver(this.handleEntries.bind(this))
    this.observer.observe({ type: "event", buffered: true })
  }

  handleEntries(list) {
    for (const entry of list.getEntries()) {
      if (entry.interactionId) {
        this.interactions.push({
          duration: entry.duration,
          type: entry.name,
          timestamp: entry.startTime,
          target: entry.target?.tagName
        })
      }
    }
  }

  getINP() {
    if (this.interactions.length === 0) return 0

    const sorted = [...this.interactions].sort(
      (a, b) => b.duration - a.duration
    )
    const index = Math.min(
      Math.floor(this.interactions.length * 0.98),
      this.interactions.length - 1
    )

    return sorted[index].duration
  }
}

Frequently Asked Questions

How is INP different from FID?

FID only measures the input delay of the first interaction, while INP measures the full duration (input delay + processing + presentation delay) of all interactions throughout the page session, reporting the worst one.

What's a realistic INP target for complex applications?

While Google recommends <200ms as "good," complex applications should:

  • Aim for <200ms on simple interactions (buttons, links)
  • Accept 200-300ms for complex operations (filters, searches)
  • Never exceed 500ms for any interaction
  • Provide immediate visual feedback for longer operations

Can scrolling affect INP?

No, scrolling is considered a continuous action and doesn't affect INP. However, scroll-triggered JavaScript that blocks other interactions can indirectly impact INP scores.

How do I optimize INP for mobile devices?

Mobile optimization requires special attention:

  • Test on real devices (3-5x slower CPUs than desktop)
  • Reduce JavaScript bundle sizes
  • Use touch-optimized event handling
  • Account for slower network speeds
  • Consider reduced motion preferences

Will improving INP hurt other metrics?

When done correctly, INP optimization improves overall performance:

  • Better code splitting can improve LCP
  • Reduced JavaScript can improve FID
  • Proper rendering optimization benefits all metrics

Balance is key—don't sacrifice initial load performance for interaction performance or vice versa.

Put GEO into practice

Generate AI-optimized content that gets cited.

Try Rankwise Free
Newsletter

Stay ahead of AI search

Weekly insights on GEO and content optimization.