# Optimizing INP: A Practical Guide to Faster Page Interactions

> Step-by-step guide to improving Interaction to Next Paint scores with task chunking, event handler optimization, and third-party script management techniques.

Canonical page: [https://tryrankwise.com/resources/guides/optimizing-inp](https://tryrankwise.com/resources/guides/optimizing-inp). Updated 2026-03-30. Published by Rankwise (tryrankwise.com).

Interaction to Next Paint (INP) measures how quickly your page responds to user interactions: clicks, taps, key presses. A Good INP score (under 200ms) means every interaction feels instant. A Poor score (over 500ms) means users experience visible lag when they try to do anything.

This guide walks through a systematic approach to diagnosing and fixing INP problems.

---

## Step 1: Measure Your Baseline

Before optimizing, know where you stand.

### Field Data (What Matters for Rankings)

Check your real-user INP in Google Search Console under Core Web Vitals → Interactivity. This data comes from Chrome User Experience Report (CrUX) and represents actual user experience over 28 days.

### Lab Testing (For Debugging)

Open Chrome DevTools → Performance panel. Record yourself interacting with the page: click buttons, open dropdowns, submit forms, type in inputs. Look for interactions where total duration exceeds 200ms.

The Web Vitals Chrome extension displays INP in real-time as you interact with the page, highlighting each interaction's duration.

---

## Step 2: Identify the Bottleneck Phase

Each interaction has three phases. Identify which one is slow:

### Input Delay

The time between the user's input and the event handler starting. This is high when the main thread is busy with other work (long tasks, third-party scripts).

**Diagnosis**: In DevTools Performance panel, look for long tasks (red corners) happening right before your interaction marker.

**Fixes**:

- Break long tasks using `scheduler.yield()` or `setTimeout(fn, 0)`
- Defer non-critical JavaScript with `async`/`defer` attributes
- Move heavy initialization to idle callbacks with `requestIdleCallback`

### Processing Time

How long the event handler itself takes to run. This is high when handlers do expensive DOM operations, complex state calculations, or synchronous network requests.

**Diagnosis**: Click on the interaction in DevTools and expand the call stack. Identify which functions take the most time.

**Fixes**:

- Debounce rapid input handlers (search-as-you-type, resize)
- Move heavy computation to Web Workers
- Avoid forced synchronous layout (reading layout properties after writing them)
- Use `requestAnimationFrame` for visual updates

### Presentation Delay

Time for the browser to recalculate styles, run layout, and paint after the handler finishes. This is high on pages with large DOM trees or complex CSS.

**Diagnosis**: In DevTools, look for long "Recalculate Style" and "Layout" entries after your event handler completes.

**Fixes**:

- Reduce DOM size (target under 1,500 nodes)
- Avoid triggering layout on large portions of the page
- Use CSS `contain` property to limit layout scope
- Prefer `transform` and `opacity` for animations (compositor-only properties)

---

## Step 3: Optimize Long Tasks

Long tasks (over 50ms) are the most common cause of poor INP because they block the main thread, preventing the browser from processing user input.

### Task Chunking with yield()

Break long synchronous operations into smaller chunks that yield to the browser:

```javascript
async function processLargeList(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i])

    // Yield every 5 items to let the browser handle interactions
    if (i % 5 === 0) {
      await scheduler.yield()
    }
  }
}
```

If `scheduler.yield()` isn't available in your target browsers, use the fallback:

```javascript
function yieldToMain() {
  return new Promise(resolve => setTimeout(resolve, 0))
}
```

### Code Splitting

Load JavaScript only when needed. Route-based and component-based code splitting prevent unused code from blocking interactions.

```javascript
// Instead of importing everything upfront
const HeavyComponent = lazy(() => import("./HeavyComponent"))
```

### Web Workers for Heavy Computation

Move CPU-intensive work off the main thread entirely:

```javascript
// main.js
const worker = new Worker("processor.js")
worker.postMessage({ data: largeDataSet })
worker.onmessage = e => updateUI(e.data)

// processor.js
self.onmessage = e => {
  const result = expensiveCalculation(e.data)
  self.postMessage(result)
}
```

---

## Step 4: Manage Third-Party Scripts

Third-party scripts (analytics, ads, chat widgets) often contribute 30-50% of main thread blocking time.

### Audit Impact

1. Open DevTools → Performance panel
2. Record a page load and several interactions
3. Group by third-party domain to see which scripts consume the most time
4. Compare INP with and without specific scripts (use Chrome's request blocking)

### Mitigation Strategies

| Strategy                    | When to Use                                            |
| --------------------------- | ------------------------------------------------------ |
| `async` or `defer`          | Scripts that don't need to run synchronously           |
| Facade pattern              | Chat widgets, video embeds (load on first interaction) |
| Tag manager delayed loading | Non-critical analytics and tracking                    |
| Remove entirely             | Scripts providing minimal value vs. performance cost   |

### Example: Lazy-Loading a Chat Widget

```html
<!-- Instead of loading immediately -->
<script>
  document.addEventListener(
    "click",
    function loadChat() {
      const script = document.createElement("script")
      script.src = "https://chat-widget.example.com/widget.js"
      document.body.appendChild(script)
      document.removeEventListener("click", loadChat)
    },
    { once: true }
  )
</script>
```

---

## Step 5: Optimize Event Handlers

### Debounce Rapid Inputs

For search boxes, resize handlers, and scroll-driven updates:

```javascript
let timeout
input.addEventListener("input", e => {
  clearTimeout(timeout)
  timeout = setTimeout(() => {
    performSearch(e.target.value)
  }, 150)
})
```

### Avoid Layout Thrashing

Reading layout properties (offsetHeight, getBoundingClientRect) after writing style changes forces the browser to recalculate layout synchronously:

```javascript
// Bad: Forces layout recalculation
element.style.width = "100px"
const height = element.offsetHeight // Forces sync layout

// Better: Read first, then write
const height = element.offsetHeight
element.style.width = "100px"
```

### Use Passive Event Listeners

For scroll and touch handlers that don't call `preventDefault()`:

```javascript
element.addEventListener("scroll", handleScroll, { passive: true })
```

---

## Step 6: Monitor and Iterate

### Set Up Real User Monitoring

Use the `web-vitals` library to track INP in your analytics:

```javascript
import { onINP } from "web-vitals"

onINP(metric => {
  analytics.track("web_vital", {
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    entries: metric.entries.map(e => ({
      name: e.name,
      duration: e.duration
    }))
  })
})
```

### Track Progress

- Check CrUX data monthly (28-day rolling window)
- Set alerts for INP regression above 200ms
- Test INP after every deploy that changes JavaScript

---

## FAQ

**How long does it take for INP improvements to show in Search Console?**
CrUX data uses a 28-day rolling window. After deploying fixes, expect 4-6 weeks for full reflection in Search Console and PageSpeed Insights.

**What's the difference between TBT and INP?**
TBT (Total Blocking Time) is a lab metric that measures total main thread blocking during page load. INP is a field metric that measures actual interaction responsiveness throughout the session. TBT is the best lab proxy for INP but they don't map 1:1.

**Should I optimize for INP or TBT?**
Both. Use TBT in development (Lighthouse, CI pipelines) as a fast feedback loop. Monitor INP in production (CrUX, RUM) as the metric that actually affects rankings.

**Can React/Next.js apps achieve Good INP?**
Yes, but it requires attention to hydration performance, state management efficiency, and avoiding expensive re-renders on interaction. React 18's concurrent features and `useTransition` help by deprioritizing non-urgent updates.
