JavaScript profiler showing long tasks

Fix High INP: Long Tasks and Event-Handler Patterns

Practical techniques for reducing Interaction to Next Paint scores by breaking up long tasks, deferring non-critical work, and restructuring event handlers.

inpjavascriptlong-tasksevent-handlersperformance

If your INP score is above 200 ms, your site feels sluggish to real users—even if everything else loads quickly. This article is a hands-on companion to the INP-first checklist and focuses on the three most common causes of high INP: long tasks, expensive event handlers, and main-thread congestion.

It is aimed at front-end developers who have already identified an INP problem using field data or lab tools and need concrete patterns to fix it. The web development hub covers the broader JavaScript ecosystem, and the web fundamentals path provides structured progression from basics to production performance.

What makes a task "long"

The browser defines a long task as any JavaScript execution that occupies the main thread for more than 50 ms. During that time, the browser cannot process user input, run animations, or paint updates. Long tasks are the single biggest contributor to high INP.

Common sources of long tasks in 2026 codebases:

  • Hydration: frameworks re-attaching event listeners and re-rendering the component tree after SSR
  • Third-party scripts: analytics, A/B testing, consent management, and chat widgets
  • State management: cascading re-renders triggered by a single state change
  • JSON parsing: large API responses parsed on the main thread

Diagnosing long tasks step by step

Step 1: Record a performance trace

Open Chrome DevTools, go to the Performance panel, and click Record. Interact with the page the way a real user would—click buttons, open menus, type in inputs. Stop recording after 10–15 seconds.

Step 2: Find the interactions track

The Interactions lane shows every user interaction with its total duration broken into input delay, processing time, and presentation delay. Red or orange bars indicate interactions that exceed the "good" threshold.

Step 3: Trace the call stack

Click on a slow interaction and examine the flame chart below it. Look for the widest bars—those are the longest-running functions. Note the file and line number.

Step 4: Categorise the cost

For each slow function, decide whether the work is:

  • Essential and synchronous: it must complete before the UI updates (e.g., validating a form field)
  • Essential but deferrable: it must happen but not in this event handler (e.g., sending an analytics event)
  • Non-essential: it should not run during this interaction at all (e.g., prefetching unrelated resources)

Fixing long tasks: practical patterns

Yielding with scheduler.yield()

The scheduler.yield() API pauses execution, lets the browser process pending input, and then resumes your code. Unlike setTimeout, it preserves the task's priority so you do not lose your place in the queue.

Use it inside loops or sequential operations where each step takes more than a few milliseconds. The pattern is:

  1. Do a chunk of work
  2. Yield
  3. Resume the next chunk

This keeps individual tasks under 50 ms while still completing the full operation.

Breaking up event handlers

If a single click handler does multiple things—update state, animate a transition, send an analytics ping, prefetch the next page—split those responsibilities:

  1. Immediate: update the UI to acknowledge the interaction (toggle a class, show a spinner)
  2. Deferred: use requestAnimationFrame for visual follow-up work
  3. Background: use requestIdleCallback or queueMicrotask for non-visual work

Debouncing expensive recalculations

For input fields that trigger search, filtering, or computation on every keystroke, debounce the expensive part while keeping the visual feedback instant. Show the typed characters immediately; run the search after 150–300 ms of inactivity.

Moving computation to Web Workers

For genuinely CPU-intensive work—image processing, complex sorting of large datasets, cryptographic operations—move the computation off the main thread entirely. Be aware of serialisation costs: transferring large objects via postMessage has its own overhead. Use Transferable objects where possible.

Event handler anti-patterns to avoid

Reading layout, then writing, then reading again. This forces synchronous layout recalculation (layout thrashing). Batch all reads first, then batch all writes.

Attaching listeners to document instead of the target. Document-level listeners fire for every interaction on the page. Attach handlers to the narrowest possible element.

Synchronous localStorage access inside handlers. localStorage.getItem and setItem are synchronous and can take several milliseconds on slow storage. Cache values in memory and write asynchronously.

Forgetting { passive: true } on scroll and touch listeners. Without the passive flag, the browser must wait for your handler to decide whether to call preventDefault() before it can scroll. This adds direct input delay.

Measuring improvement

After applying fixes, measure in both lab and field:

  • Lab: re-record a Performance trace. Confirm no task in the interaction path exceeds 50 ms. Check that the Interactions track shows green.
  • Field: deploy and wait for 7 days of CrUX data. Compare the p75 INP before and after. You need at least 28 days of data for stable CrUX results.
  • Ongoing: add a performance test to your CI pipeline that runs Lighthouse or Web Vitals on key pages and fails the build if INP regresses past your threshold.

Trade-offs

  • scheduler.yield() is not yet available in all browsers. You may need a polyfill or fallback to setTimeout.
  • Web Workers add architectural complexity. Do not use them for trivial computations where simple debouncing would suffice.
  • Aggressive code splitting helps initial load but can make interactions slower if the user triggers a lazy import. Preload modules you know will be needed.

Further reading on EBooks-Space