<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" />

INP is the Interaction Metric that Matters: A Practical Fix-It Guide

Core Web VitalsPerformanceJavaScriptWeb Dev
Improving Interaction to Next Paint (INP) for better UX and SEO

TL;DR: If clicks or taps feel laggy, your Interaction to Next Paint (INP) is probably too high. This guide shows practical ways to bring it down—budget long tasks, prioritise input work, split JavaScript, and keep the main thread clear—plus a checklist, stats table and FAQs you can use straight away.

What is INP (Interaction to Next Paint)?

INP measures how quickly the page responds visually after a user interaction (like tap, click, or key press). Lower INP means the UI actually feels responsive. As interaction-heavy sites have grown, INP is now a key quality signal for user experience and search visibility.

Why INP matters for performance and SEO

  • Real UX impact: Fast taps and clicks reduce abandonment and increase conversions.
  • Signals quality: Search systems increasingly value interaction quality alongside loading and visual stability.
  • Easy to diagnose: DevTools, field data, and RUM help pinpoint the slow interactions quickly.

Quick wins: the INP improvement checklist

  1. Surface input work first: Use scheduler.postTask (where available) or microtasks to prioritise user input.
  2. Break long tasks: Split any task over ~50 ms. Defer non-critical work with requestIdleCallback or after the next paint.
  3. Hydration smarter, not harder: Islands/partial hydration; avoid blocking the main thread with large client bundles.
  4. Preconnect & prefetch wisely: Establish critical connections early; lazy-load anything the user may never see.
  5. Minimise layout thrash: Batch DOM writes/reads and avoid style recalcs during interaction.
  6. Use bfcache-friendly patterns: Don’t disable back/forward cache with unload handlers or intrusive listeners.

Stats table: INP targets and typical fixes

Area Good practice Impact on INP
Long tasks Split >50 ms tasks; defer non-critical work Reduces interaction blocking
Input priority Handle input first, render next, work later Faster visual feedback
Hydration Island/partial hydration; code-split routes Keeps main thread responsive
Network Preconnect critical domains; lazy-load non-critical Reduces time to ready state

Mini methodology & example dataset

The figures below illustrate a simple, reproducible approach to measuring and improving INP in production.

How we measured

  • Window: 14 days of field data in September 2025.
  • Scope: 3 production websites with interaction-heavy UI (forms, menus, filters).
  • Collection: Real User Monitoring (RUM) via the PerformanceEventTiming API, sampled at ~10% of page views.
  • Metric: INP p75 (75th percentile), segmented by device class and route.
  • Exclusions: Bots, background tabs, and interactions <16 ms (noise floor).
  • Changes deployed: long-task splitting (<50 ms targets), input-first handlers, partial hydration, and deferring non-critical work until after paint.

Example results (illustrative)

Site Device INP p75 (Before) INP p75 (After) Primary change
Site A Mobile 280 ms 160 ms Split long tasks; defer analytics
Site B Desktop 190 ms 120 ms Input-first handlers; batch DOM work
Site C Mobile 350 ms 210 ms Partial hydration; route-level code-split

These numbers are example placeholders. Replace them with your own RUM figures and briefly describe the exact changes deployed on your site.

Step-by-step: fix an interaction bottleneck

  1. Profile the problematic click/tap
    Open DevTools Performance. Record an interaction that feels slow. Look for long tasks, layout thrash, and script evaluation spikes.
  2. Cut long tasks into chunks
    Refactor heavy loops/work into smaller async pieces. Move non-urgent work after paint using requestAnimationFrame then setTimeout(0) or requestIdleCallback.
  3. Prioritise input handling
    Ensure the handler does the minimum: update state required for UI feedback; kick longer work to a queued task.
  4. Defer hydration where possible
    Render static HTML fast; progressively hydrate only interactive parts (islands). Code-split components by route and by interaction.
  5. Audit third-party code
    Delay tag execution until interaction or consent; remove unused SDKs; self-host critical assets where appropriate.
  6. Verify in the field
    Instrument RUM to capture INP on real devices; alert when p95/p98 crosses your budget.

Minimal example: splitting a long task


// BEFORE: one big blocking loop
button.addEventListener('click', () => {
  heavyWork(); // blocks for hundreds of ms
  renderUI();
});

// AFTER: yield to the browser between chunks
button.addEventListener('click', async () => {
  await Promise.resolve(); // let input/paint run
  for (const chunk of chunksOf(heavyWorkItems, 50)) {
    process(chunk);
    await new Promise(requestAnimationFrame); // yield between pieces
  }
  renderUI();
});
  

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

Improve INP on a WordPress site (practical examples)

INP issues on WordPress are often caused by heavy front-end JavaScript, third-party tags, and plugin scripts/styles loading on every page. Use the patterns below to keep the main thread clear at the moment of interaction.

1) Only load scripts where they’re needed

Dequeue non-critical assets on templates that don’t need them. This reduces parse/compile time and keeps interactions snappy.

/* functions.php or a small mu-plugin */
add_action('wp_enqueue_scripts', function () {
  // Example: disable a slider script except on the homepage
  if ( ! is_front_page() ) {
    wp_dequeue_script('my-slider');        // handle from where it was registered
    wp_dequeue_style('my-slider-styles');
  }

  // Example: only load CF7 on pages that actually have a form
  if ( ! is_page(array('contact', 'quote')) ) {
    wp_dequeue_script('contact-form-7');
    wp_dequeue_style('contact-form-7');
  }
}, 100);

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

2) Defer or async non-critical JavaScript

Give parsing/execution room to render and handle input first. Use a filter to add defer or async to selected handles.

/* Add defer/async attributes safely */
add_filter('script_loader_tag', function ($tag, $handle, $src) {
  const defer = ['my-slider','analytics-lib'];  // add your handles here
  const async = [];                             // or here if truly independent

  if (in_array($handle, $defer, true)) {
    return '<script src="'.esc_url($src).'" defer></script>';
  }
  if (in_array($handle, $async, true)) {
    return '<script src="'.esc_url($src).'" async></script>';
  }
  return $tag;
}, 10, 3);

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

3) Reduce WooCommerce cart fragments impact

wc-cart-fragments can wake the main thread on pages without a cart. Disable it where it isn’t needed.

/* Disable cart fragments off cart/checkout/account */
add_action('wp_enqueue_scripts', function() {
  if (function_exists('is_woocommerce') && ! is_cart() && ! is_checkout() && ! is_account_page()) {
    wp_dequeue_script('wc-cart-fragments');
  }
}, 99);

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

4) Move low-value work after paint

Kick non-essential initialisers to idle time so interactions and the first paint win.

<script>
(function () {
  var boot = function () {
    // initialise non-critical widgets, analytics, etc.
    // keep each task < 50ms or split into chunks
  };
  if ('requestIdleCallback' in window) {
    requestIdleCallback(boot, { timeout: 2000 });
  } else {
    setTimeout(boot, 1000);
  }
})();
</script>

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

5) Split long tasks in your theme/plugin JS

If a click runs heavy code, yield between chunks so the browser can paint. This directly improves INP.

button.addEventListener('click', async () => {
  await Promise.resolve(); // let the browser handle input/paint first
  for (const batch of chunk(items, 50)) {
    processBatch(batch);
    await new Promise(requestAnimationFrame); // yield between chunks
  }
  renderUI();
});

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

6) Keep the back/forward cache (bfcache) working

Avoid unload handlers and heavy, page-wide listeners. Use pagehide instead of beforeunload where possible, and scope listeners to components.

7) Trim render-blocking CSS & images

  • Critical CSS: inline only what’s above the fold; defer the rest with media="print" onload or modern CSS splitting.
  • Images: serve WebP/AVIF where supported; set width/height; lazy-load below the fold; avoid layout shifts on interaction.
  • Fonts: use font-display: swap and preconnect to your font host.

8) Curb third-party tags until consent

Load marketing/analytics tags after user consent and away from initial interaction using your CMP or Consent Mode hooks. This removes a frequent source of long tasks.

9) Server & database hygiene (helps main-thread indirectly)

  • Page/object cache: enable full-page caching and a persistent object cache (e.g., Redis) to cut TTFB variance.
  • Autoloaded options: audit and reduce oversized autoloaded rows to avoid slow bootstraps.
  • HTTP/2/3 + CDN: faster connection reuse and edge caching improve time-to-ready for interactive scripts.

10) WordPress-specific housekeeping

/* Disable emojis/oEmbed if unused (small but tidy) */
add_action('init', function () {
  remove_action('wp_head', 'print_emoji_detection_script', 7);
  remove_action('wp_print_styles', 'print_emoji_styles');
  remove_action('wp_head', 'wp_oembed_add_discovery_links');
  remove_action('wp_head', 'wp_oembed_add_host_js');
});

Caveat: Do not deploy this code on a live environment. Always test on a staging site first before deploying to a live production site.

After each change, test an actual click in DevTools → Performance. Look for fewer & shorter long tasks around the interaction and a lower INP in your RUM dashboard.

Frequently asked questions (INP & interaction performance)

What’s a good INP score?

Lower is better. Focus on reducing long tasks and ensuring visible feedback quickly after each interaction.

Is INP only about JavaScript?

No. JavaScript is a common cause, but layout thrash, heavy styles, and blocking resources also harm interaction.

Do I need a new framework to improve INP?

Not necessarily. Most wins come from splitting work, shipping less JavaScript, and prioritising input with your current stack.

How do I measure INP on real users?

Use a Real User Monitoring setup that captures interactions and reports percentiles (e.g., p75/p95) by device and route.

Will lazy-loading everything fix INP?

Lazy-load non-critical features, but keep above-the-fold interactions fast and ready. Defer only what users may never touch.

Need help improving INP?

If you’d like us to help to audit your site and produce an INP improvement plan, get in touch.

Share: