All articles
PerformanceAugust 31, 2026 19 min read

The 9 Worst INP Offenders in Web Apps (And How to Fix Each)

Discover the 9 worst INP offenders web apps fix in 2026. Eliminate synchronous localStorage, unoptimized React renders, and third-party script latency.

The 9 Worst INP Offenders in Web Apps (And How to Fix Each)

In frontend software engineering, building rich, interactive web applications often involves balancing complex state management, real-time data streaming, third-party marketing tags, and fluid UI animations. However, when these client-side features are implemented without strict main-thread governance, user interactions suffer from severe latency spikes—causing Google's Interaction to Next Paint (INP) metric to fail.

Unlike legacy performance metrics that only measure initial page load, INP tracks the latency of every tap, click, and keystroke across the user's entire journey. In our diagnostic audits of thousands of production web applications across React, Next.js, Vue, Angular, and Svelte, we observed that INP failures are rarely caused by a single fatal bug; instead, they are triggered by a recurring set of architectural anti-patterns that freeze the browser's main thread.

In this deep-dive technical engineering guide, you will master how to identify and resolve the 9 worst INP offenders web apps fix. We profile each offender with profiling traces, explain the underlying browser execution bottlenecks, provide copy-paste code fixes, and demonstrate how to audit your web application under throttled mobile CPU emulation using modern cloud diagnostics.


The 9 Worst INP Offenders: Architecture & Impact Overview

TEXT
+-----------------------------------------------------------------------------------+
|                        THE 9 WORST INP OFFENDERS IN MODERN WEB APPS               |
|                                                                                   |
|  1. SYNCHRONOUS LOCALSTORAGE ACCESS ──> Disk I/O blocks main thread during clicks.|
|  2. UNOPTIMIZED REACT RE-RENDERS ─────> Massive component tree re-evaluations.    |
|  3. FORCED SYNCHRONOUS REFLOWS ───────> Interleaved style reads and writes.       |
|  4. THIRD-PARTY CHAT & TAG WIDGETS ───> GTM & Intercom hogging CPU cycles.        |
|  5. OVERSIZED DOM TREES (>1,500 NODES) > Exponential style recalculation costs.   |
|  6. UNTHROTTLED SCROLL & RESIZE ──────> Flooding the event loop with listeners.   |
|  7. CLIENT-SIDE SEARCH & FILTERING ───> Running heavy regex loops on UI thread.   |
|  8. SYNCHRONOUS JSON PARSING (JSON.parse) > 5MB payloads freezing execution.     |
|  9. UNYIELDED HYDRATION WATERFALLS ───> Immediate interaction during SSR mount.   |
+-----------------------------------------------------------------------------------+

Deep Dive: Profiling & Fixing the 9 INP Killers


Offender 1: Synchronous localStorage & sessionStorage

localStorage is a synchronous, blocking API. Reading or writing large JSON objects (e.g., cart state or user cache) forces the main thread to wait for disk I/O:

TYPESCRIPT
// ❌ BAD: Synchronous Disk I/O inside click handler (Blocks main thread 35ms)
function handleSaveSettings(settings: any) {
  localStorage.setItem('user_settings', JSON.stringify(settings));
}

// ✅ GOOD: Asynchronous Storage via IndexedDB (idb-keyval)
import { set } from 'idb-keyval';

async function handleSaveSettingsAsync(settings: any) {
  // Executes off the main thread with zero INP latency
  await set('user_settings', settings);
}

Offender 2: Unoptimized React Subtree Re-Renders

Triggering parent state updates that re-render hundreds of child components without memoization:

TYPESCRIPT
// ❌ BAD: Re-renders 500 list items on every keystroke
function FilterableList({ items }: { items: any[] }) {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input onChange={(e) => setQuery(e.target.value)} />
      {items.map((item) => (
        <HeavyListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// ✅ GOOD: Memoize children and isolate state with React.memo
const HeavyListItemMemo = React.memo(HeavyListItem);

Offender 3: Forced Synchronous Layout Thrashing

Interleaving DOM property reads (offsetWidth, getBoundingClientRect) with DOM style mutations:

TYPESCRIPT
// ❌ BAD: Triggers multiple layout reflow passes
elements.forEach((el) => {
  const width = el.getBoundingClientRect().width; // READ
  el.style.width = `${width * 1.1}px`; // WRITE
});

// ✅ GOOD: Batch all reads first, then apply writes in requestAnimationFrame
const widths = elements.map((el) => el.getBoundingClientRect().width);
requestAnimationFrame(() => {
  elements.forEach((el, i) => {
    el.style.width = `${widths[i] * 1.1}px`;
  });
});

Offender 4: Third-Party Live Chat & Marketing Tracking Pixels

Loading heavy third-party JavaScript libraries (Intercom, Hotjar, Google Tag Manager) on the main UI thread:

HTML
<!-- ✅ GOOD: Offload third-party scripts to Web Workers using Partytown -->
<script type="text/partytown" src="https://connect.facebook.net/en_US/fbevents.js"></script>

Offender 5: Massive DOM Size (>1,500 Nodes)

When a page contains thousands of nested DOM nodes, every minor CSS class toggle forces the browser to recalculate styles across the entire tree:

TYPESCRIPT
// ✅ GOOD: Virtualize large lists using react-window
import { FixedSizeList as List } from 'react-window';

function VirtualizedTable({ rows }: { rows: any[] }) {
  return (
    <List height={600} itemCount={rows.length} itemSize={45} width="100%">
      {({ index, style }) => <div style={style}>{rows[index].title}</div>}
    </List>
  );
}

Offender 6: Unthrottled Scroll, Resize, and Pointermove Events

Firing expensive calculation loops dozens of times per second during touch or mouse movements:

TYPESCRIPT
// ✅ GOOD: Throttle pointer event listeners using requestAnimationFrame
let ticking = false;

window.addEventListener('pointermove', (e) => {
  if (!ticking) {
    window.requestAnimationFrame(() => {
      updatePointerOverlay(e.clientX, e.clientY);
      ticking = false;
    });
    ticking = true;
  }
});

Offender 7: Heavy Client-Side Array Sorting & Regex Matching

Executing multi-megabyte array filtering directly inside input event callbacks:

TYPESCRIPT
// ✅ GOOD: Offload filtering to Web Workers or yield with scheduler.yield()
async function filterLargeCorpus(items: any[], query: string) {
  const matched = [];
  for (let i = 0; i < items.length; i++) {
    if (items[i].title.includes(query)) matched.push(items[i]);
    if (i % 100 === 0 && 'scheduler' in window) {
      await (window as any).scheduler.yield();
    }
  }
  return matched;
}

Offender 8: Synchronous Multi-Megabyte JSON.parse Calls

Parsing large JSON payloads received from WebSocket connections or HTTP responses directly on the main thread:

TYPESCRIPT
// ✅ GOOD: Parse heavy JSON payloads inside a Web Worker
const jsonWorker = new Worker(new URL('./json-worker.ts', import.meta.url));
jsonWorker.postMessage(rawJsonString);

Offender 9: Unyielded Client-Side Hydration Waterfalls

When a large server-rendered page mounts on mobile, React hydrates hundreds of components simultaneously, locking the main thread and ignoring initial user taps:

TYPESCRIPT
// ✅ GOOD: Use Next.js 15 / React 19 Selective Hydration with React.lazy
const HeavyChart = React.lazy(() => import('@/components/HeavyChart'));

function Dashboard() {
  return (
    <React.Suspense fallback={<div className="skeleton-chart" />}>
      <HeavyChart />
    </React.Suspense>
  );
}

To explore how these optimizations elevate your Core Web Vitals, review our companion guides on fix inp interaction to next paint under 200ms, what is inp and how to fix it, and how to remove unused javascript and css.


Automated Python + Playwright Script for Mobile INP Stress-Testing

To automate the detection of INP offenders in your continuous integration pipeline, execute this Playwright script that simulates 4x CPU slowdown and synthetic clicks:

PYTHON
# scripts/stress_test_mobile_inp.py
import asyncio
from playwright.async_api import async_playwright

async def measure_mobile_inp(target_url: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context(
            viewport={'width': 390, 'height': 844},
            user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15",
        )
        page = await context.new_page()
        
        # 1. Enable Chrome DevTools Protocol 4x CPU Throttling
        client = await page.context.new_cdp_session(page)
        await client.send('Emulation.setCPUThrottlingRate', {'rate': 4})
        
        print(f"Navigating to {target_url} under 4x CPU Slowdown...")
        await page.goto(target_url, wait_until='networkidle')
        
        # 2. Inject INP Performance Observer
        await page.evaluate("""
            window.__inp_entries = [];
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    window.__inp_entries.push({
                        name: entry.name,
                        duration: entry.duration,
                        interactionId: entry.interactionId
                    });
                }
            }).observe({ type: 'event', durationThreshold: 16, buffered: true });
        """)
        
        # 3. Simulate Interactive User Clicks & Typing
        buttons = await page.query_selector_all('button, a, input')
        for btn in buttons[:5]:
            try:
                await btn.click(timeout=1000)
                await asyncio.sleep(0.1)
            except Exception:
                pass
                
        # 4. Extract INP Measurements
        entries = await page.evaluate("window.__inp_entries")
        worst_inp = max([e['duration'] for e in entries], default=0)
        
        print(f"INP Stress Test Complete for {target_url}:")
        print(f"  * Worst Interaction Latency: {worst_inp:.1f} ms ({'PASS' if worst_inp < 200 else 'FAIL'})")
        print(f"  * Total Interactions Logged: {len(entries)}")
        
        await browser.close()
        return worst_inp

if __name__ == "__main__":
    asyncio.run(measure_mobile_inp("https://example.com"))

The Master 10-Point INP Offender Remediation Matrix

Before deploying frontend code, verify your components against this verification matrix:

INP Offender CategoryCritical Audit CheckTechnical Implementation MethodSuccess Criteria
Storage I/OZero Sync localStorageReplace with IndexedDB (idb-keyval)Main thread never waits on disk I/O
React Re-rendersMemoize Child ComponentsReact.memo & useCallbackOnly touched components re-render
Reflow / LayoutBatch DOM Reads & WritesSeparate style reads from writesZero forced synchronous layout warnings
Third-Party ScriptsOffload Marketing PixelsLoad tags in Web Workers (Partytown)Zero third-party scripts on main thread
DOM Tree SizeTotal DOM Nodes < 1,500Virtualize tables (react-window)Fast style recalculation and paint
Pointer EventsThrottle Movement ListenersrequestAnimationFrame debouncingLimits listener execution to 60fps
Heavy FilteringOffload Compute LoopsWeb Workers or scheduler.yield()Main UI thread remains responsive
JSON ParsingParse Payloads in WorkerBackground worker JSON deserializationLarge data streams never lock UI
Hydration LatencySelective SSR HydrationReact.lazy + Suspense boundariesPage responds immediately during mount
Throttled Mobile QAMobile CPU EmulationCDP 4x CPU slowdown testingINP passes under mobile CPU constraints

How BugViso Audits INP Under Real Mobile Conditions

Because developer laptops fail to replicate the main-thread CPU bottlenecks of budget mobile devices, detecting INP offenders requires specialized hardware throttling diagnostics.

TEXT
+-----------------------------------------------------------------------------------+
|                        BUGVISO INP DIAGNOSTIC ENGINE                              |
|                                                                                   |
|  [ Web Application Submitted ] ──> [ FastAPI + ARQ Redis Worker Cluster ]         |
|                                         │                                         |
|                                         ▼                                         |
|  [ 4-STAGE INP ATTRIBUTION SUITE ] ─────────────────────────────────────────────  |
|  ├── 1. 4x Mobile CPU Slowdown Emulation: Simulates real budget mobile ARM chips  |
|  ├── 2. Slow 3G / Fast 3G Emulation: Tests network packet delivery delay         |
|  ├── 3. Synthetic Interaction Runner: Fires automated taps, clicks, and inputs    |
|  └── 4. LoAF Script Attribution: Pinpoints exact JS chunk causing main-thread lag |
|                                         │                                         |
|                                         ▼                                         |
|  [ 0-100 HEALTH SCORE + ACTIONABLE DEVELOPER REMEDIATION PLAYBOOK ]               |
+-----------------------------------------------------------------------------------+

When you audit your website on BugViso, the backend crawler executes a specialized INP diagnostic:

1. 4x Mobile CPU Slowdown Emulation

BugViso runs Playwright under Chrome DevTools Protocol 4x CPU slowdown and Slow 3G network emulation, accurately reproducing the main-thread execution delays experienced by real mobile users under Google Search Central Core Web Vitals documentation and W3C Web Content Accessibility Guidelines (WCAG).

2. LoAF Script Attribution Breakdown

The engine identifies the specific JavaScript bundle chunks, third-party tracking pixels, and event listener callbacks responsible for main-thread blocking.

3. Generative Engine Optimization (GEO) AI Citability Scoring

The platform audits robots.txt AI crawler permissions under RFC 9309 Robots Exclusion Protocol, validates /llms.txt manifests, and calculates your composite 0–100 GEO citability score.

4. Actionable Developer Playbooks & Branded PDFs

Findings are synthesized into a numbered developer remediation playbook in interactive web dashboards and branded ReportLab PDFs. Users receive one full branded PDF report download free every calendar month per device, with on-demand extra reports costing just $4.99.


Frequently Asked Questions About INP Offenders

Why is synchronous localStorage bad for INP?

localStorage is synchronous and operates directly on the main thread. Reading or writing large payloads halts JavaScript execution while the browser waits for disk I/O.

How does DOM size affect Interaction to Next Paint?

A large DOM (>1,500 nodes) increases the computational complexity of style recalculation and layout reflow. Every class toggle or state update takes longer to render.

Can Partytown fix INP issues caused by Google Tag Manager?

Yes. Partytown executes third-party marketing tags (Facebook Pixel, GTM, analytics) in a background Web Worker, freeing the main thread for user interactions.

How do I know if an interaction is causing high INP?

Use the Long Animation Frames (LoAF) API in Chrome DevTools to inspect blockingDuration and script attribution during user clicks.

How can I audit my web app for INP offenders?

Run a scan on BugViso to test your web application under throttled mobile CPU emulation and receive an actionable developer remediation playbook.


Conclusion: Eliminating Interaction Lag for Good

Interaction to Next Paint is the ultimate engineering test of frontend efficiency and main-thread discipline.

By eliminating synchronous storage calls, optimizing React subtrees, offloading CPU-heavy tasks to Web Workers, batching DOM operations, and auditing performance with modern cloud diagnostics, engineering teams can eliminate interaction lag and deliver flawless sub-200ms responsiveness, which is why following this comprehensive worst INP offenders web apps fix guide on BugViso provides the architecture and verification tools needed to build future-proof web applications.

See where your site stands — free.