f in x
INP Optimization: How to Reduce JavaScript Interactivity and Free the Main Thread
> cd .. / HUB_EDITORIALE
Seo e analitica

INP Optimization: How to Reduce JavaScript Interactivity and Free the Main Thread

[2026-06-14] Author: Ing. Calogero Bono

Have you ever clicked a button and waited half a second before anything happened? Or worse, tapped a link on mobile and the screen froze for a second? That sluggish feeling isn't just annoying — it's a technical problem called INP (Interaction to Next Paint), one of the Core Web Vitals. If your site has high INP, Google penalizes it in rankings. Many developers ignore it until they see a performance crash. At Meteora Web, we've seen dozens of sites slow down exactly because of blocking JavaScript and a clogged main thread. This guide explains why it happens and how to fix it, with real code and actionable steps.

What is INP and Why the Main Thread is the Bottleneck

INP measures the time between a user interaction (click, tap, keypress) and the moment the browser can paint the next frame. Google considers good INP under 200 ms, a warning zone between 200–500 ms, and poor above 500 ms.

The main culprit is the main thread. The browser runs HTML, CSS, JavaScript, layout calculations, and painting on a single thread. If your JavaScript is too heavy or poorly organized, everything blocks. The result? The user clicks, but the browser is busy processing code or performing reflow, so it can't respond.

Real-life example: A client had an e-commerce cart that, on clicking “Add”, triggered multiple API calls, DOM updates, and discount calculations — all in one long JavaScript task. Mobile INP was over 700 ms. We moved non-critical operations into requestIdleCallback and fragmented the rest into micro-tasks. INP dropped to 180 ms. Conversions grew by 12% without any other changes.

Sponsored Protocol

How to Diagnose INP: Tools and Metrics

Before optimizing, you need to measure. Don't rely solely on PageSpeed Insights (averages). Look at real user behavior. We use three tools:

  • Chrome DevTools > Performance: record a flow, identify long tasks (>50 ms) blocking the main thread.
  • Web Vitals Library: attach onINP() to capture field data (RUM).
  • Search Console > Core Web Vitals: see if Google flags INP issues on specific URLs.

Once you find a long task, inspect the timeline: “Task” with duration >50 ms. Click on it to see what caused it — a function, a loop, a heavy event listener.

Optimizing the Main Thread: 3 Core Techniques

Here's what we do when the main thread is overloaded. Each technique has a clear rationale.

1. Split Long Tasks with yield and setTimeout

A long task blocks the main thread for more than 50 ms. If you have a loop processing 1000 items, the browser can't respond to clicks during that loop. Break the work into chunks and let the browser handle interactions between chunks.

function processLargeArray(items) {
  let index = 0;
  const chunkSize = 50;

  function processChunk() {
    const end = Math.min(index + chunkSize, items.length);
    for (let i = index; i < end; i++) {
      // process items[i]
    }
    index = end;

    if (index < items.length) {
      // Yield control to the browser before next chunk
      setTimeout(processChunk, 0);
    }
  }

  processChunk();
}

Why it works: setTimeout(fn, 0) doesn't execute immediately — it queues the callback. Meanwhile, the browser can handle user events (click, scroll). It's basic scheduling magic.

Sponsored Protocol

2. Delegate Heavy Work to a Web Worker

Web Workers run scripts in a separate thread. They don't block the main thread. Perfect for heavy calculations (image processing, JSON parsing, cryptography) or periodic operations that don't need instant results.

// worker.js
self.addEventListener('message', (e) => {
  const data = e.data;
  const result = heavyCalculation(data);
  self.postMessage(result);
});

function heavyCalculation(data) {
  // simulate heavy work
  let sum = 0;
  for (let i = 0; i < 100000000; i++) {
    sum += Math.sqrt(i);
  }
  return sum;
}

// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
  console.log('Result from worker:', e.data);
};

Caution: Workers have no DOM access. You cannot update UI inside a worker. But you can offload heavy computation and then update the DOM quickly on the main thread after receiving the result.

3. Reduce Event Listeners and Reflows

A common mistake: attaching an event listener to every item in a list, especially on dynamically moving elements. Each listener consumes resources and can trigger reflows (when you change dimensions or positions).

Solution: Use event delegation (a single listener on the container) and batched DOM updates.

Sponsored Protocol

// Instead of 100 individual listeners:
document.querySelectorAll('.item').forEach(el => {
  el.addEventListener('click', handleClick);
});

// Use a single listener on the container:
const container = document.getElementById('list-container');
container.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (item) {
    handleClick(item);
  }
});

// Batched updates: collect and apply in one shot
function updateUI(items) {
  // Read properties once (avoid forced reflows)
  const heights = items.map(el => el.getBoundingClientRect().height);
  // Then write in batch
  requestAnimationFrame(() => {
    items.forEach((el, i) => {
      el.style.height = (heights[i] * 2) + 'px';
    });
  });
}

Mistakes to avoid: mixing reads and writes in a loop (e.g., el.offsetHeight + 'px' inside a for with el.style.width). Every forced read of a property like offsetHeight after a write causes a synchronous reflow. Do all reads first, then all writes.

JavaScript Lazy Loading and Code Splitting for INP

Often the main thread is blocked not by code you need immediately, but by code that isn't required for the initial interaction. Bundling everything into one file is the surest way to raise INP.

Code Splitting (via Webpack, Vite, or dynamic import()) loads only the code needed for the current page. For interactive components (modals, sliders, dynamic forms), load them via import() upon interaction.

Sponsored Protocol

// Load module only when user clicks
button.addEventListener('click', async () => {
  const { showModal } = await import('./modal.js');
  showModal();
});

Lazy loading scripts (with async or defer attributes): non-render-blocking scripts are queued without halting the main thread.



Why it works: async downloads and runs as soon as ready but does not block HTML parsing. defer runs after the document is fully parsed. In both cases, the main thread doesn't stop to download the script.

Specific INP Optimization for Mobile

On mobile the problem is amplified: less powerful CPUs, often fanless, and slower connections. The main thread is more fragile. Extra rules we apply to mobile-first projects:

  • Limit touch event listeners: use passive: true to avoid blocking scroll.
  • Avoid heavy setInterval: prefer requestAnimationFrame for animations, and setTimeout with visibility checks (Intersection Observer) for periodic updates.
  • Compress and defer fonts: fonts loaded via @font-face can block rendering. Use font-display: swap and load only the subset needed for interactive parts.
  • Handle data fetching asynchronously: API calls are async, but if you then do a long synchronous loop parsing JSON while blocking, the damage is the same.

Advanced Measurement and Debug Tools

Beyond the mentioned tools, for INP we specifically use:

  • Lighthouse Report (Chrome): select “Performance” and look at “Interaction to Next Paint”. Sometimes it gives specific suggestions.
  • WebPageTest: the “Filmstrip” tab shows exactly when interactions occur and the delay before visual response.
  • Performance Observer API: track metrics in real time.
// Example PerformanceObserver for INP
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('INP entry:', entry.duration, entry.name);
  }
});
observer.observe({ type: 'first-input', buffered: true });
// For full INP (not just first input) use type: 'event' with durationThreshold >= 0

In Summary — What to Do Now

  1. Measure real INP: use Search Console and Web Vitals library to identify problematic pages.
  2. Identify long tasks: open DevTools, record an interactive profile, look for tasks >50 ms.
  3. Split and delegate: break long loops with setTimeout or move computations to Web Workers.
  4. Optimize event listeners: delegate, avoid forced reflows, use passive for touch.
  5. Apply code splitting and lazy loading: load only the JavaScript necessary for the first interaction.
  6. Test on a real device: emulation isn't enough. Try on a mid-range phone with 3G connection.

INP isn't a one-time fix. It's continuous friction: every new script, every new interaction can worsen it. At Meteora Web, we check this metric every week on projects we manage, because a millisecond saved on the main thread is a customer who doesn't abandon the cart.

Sponsored Protocol

For the full context on web performance, read our Core Web Vitals and PageSpeed Pillar Guide.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()