Reference guide · performance · Published 2026-08-15 · 3 min read
Improving INP (Interaction to Next Paint)
Improve your INP under 200ms: understand what the metric measures, break up long main-thread work, and load JavaScript only when interactions need it.
- ·What INP measures
- ·Main thread vs delay
- ·Scheduling
What INP measures
Interaction to Next Paint measures the longest time between a user interaction, a tap, click, keypress, or hover, and the next paint after it. Unlike FID, which only counted the input delay before event handlers ran, INP covers the whole interaction: the delay while the main thread is busy, plus the time the handler and everything it schedules take to repaint. Good is 200ms or less; needs improvement up to 500ms; poor above that. The threshold for Core Web Vitals is the 75th percentile of the slowest interactions, typically near the worst-performing page.
Main thread vs input delay
The dominant cause of poor INP is a busy main thread. The browser runs rendering and most JavaScript on a single thread. When it is occupied, every input event sits in a queue and waits. That waiting, called input delay, is the largest chunk of a slow INP.
The three concrete sources, in the order they appear:
| Source | Why it happens | Cut it with |
|---|---|---|
| Long tasks | One script run of 50ms or more blocks everything | Split work, lazy script, defer |
| Third-party scripts | Analytics, chat, widgets on every page | Async load, block or delay them |
| Heavy handlers | A click handler that does layout or network work | Make handlers light, do work off the critical chunk |
Fix the main thread first
- Do not ship render-blocking JavaScript. Any script you can load with
deferorasyncmoves its parse and execution off the critical path. The render-blocking resources page covers both attributes and when each is right. - Break long tasks. A handler that processes a list, reflows, or fetches should yield to rendering with
await new Promise(r => setTimeout(r, 0))between chunks, or be moved to a Web Worker for pure computation. - Delay third-party work. Analytics that run at window load can afford to wait for idle time. One rule: the third party responsible for the regression is the one that runs expensive layout or fetches megabytes into the page.
Lazy script strategies
- Defer the vendor bundle. If a feature is used outside the first screen, it does not belong in the load script.
- Code-split so a bundle's parts ship only for the view or action that needs them. A "modules" folder with dynamic
import()entries is the usual shape. - For search and filter controls, dynamic import runs only when the control is used, which keeps
loadandidletasks free.
Trim event listeners
Every listener attached at page set-up fires on its event even if it does nothing useful. Common patterns:
- A listener attached to every element that ends up doing layout work for a row that becomes invisible.
- Listeners that read geometry (
getBoundingClientRect) on scroll, which force layout work per frame. - Handlers on
window.resizeandscrollthat recompute a whole component on every event.
Attach handlers with { passive: true } when they do not call preventDefault, and remove them when the element leaves. For scroll and resize at least debounce or throttle to a frame.
The scheduler
The browser's scheduler can run long callback functions without yielding: setTimeout and requestAnimationFrame for the next frame, plus scheduler.postTask when that API is available and on a supported browser. The mental model worth keeping is the queue: input events wait behind every task, so a page that keeps its script work small and yields to the event loop will keep INP under 200ms even while scripting.