How would you improve Interaction to Next Paint (INP)
INP measures the worst end-to-end latency from any user interaction to the next paint. Improvements: split long tasks (`< 50ms`), defer non-urgent work (`requestIdleCallback`, React 18 `startTransition`), debounce/throttle handlers, offload to Web Workers, virtualize long lists, avoid layout thrash, and reduce hydration cost (RSC, partial hydration). p75 INP < 200ms is good.
INP replaced FID in 2024 as a Core Web Vital. It measures the worst interaction latency in a session — from input to next paint. Hitting p75 < 200ms is the goal; > 500ms is poor.
What contributes to INP
- Input delay — main thread busy, can't handle the event.
- Processing time — your handler runs (state update, network call, etc.).
- Presentation delay — rendering + paint after the handler.
Levers
1. Break up long tasks
The main thread should never block > 50ms.
async function processBatch(items) {
for (let i = 0; i < items.length; i += 100) {
doWork(items.slice(i, i + 100));
await new Promise((r) => setTimeout(r)); // yield
}
}Or scheduler.yield() (newer API).
2. Defer non-urgent updates (React 18)
const [filter, setFilter] = useState("");
const [list, setList] = useState(...);
function onChange(e) {
setFilter(e.target.value); // urgent
startTransition(() => setList(filterList(...))); // non-urgent
}The input updates immediately; the heavy filter is interruptible.
3. useDeferredValue for derived state
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);4. Debounce / throttle
For high-frequency events (scroll, resize, type-ahead), don't run heavy work per event. Debounce keyup; throttle scroll.
5. Web Workers
Move expensive compute (parsing big JSON, image processing, search indexing) off the main thread. Comlink makes this ergonomic.
6. Virtualize long lists
10k DOM nodes = guaranteed jank. TanStack Virtual or react-window keep only visible rows.
7. Avoid layout thrash
Reading layout properties (offsetWidth, getBoundingClientRect) after writes forces sync layout. Batch reads then writes.
8. Reduce hydration cost
- React Server Components ship less JS.
- Partial hydration (
use clientonly where needed). - Islands architecture (Astro, Qwik).
9. CSS containment
contain: layout style paint on isolated UI sections cuts the layout/paint scope.
10. Measure
import { onINP } from "web-vitals";
onINP(({ value }) => sendToAnalytics({ name: "INP", value }));Use Chrome DevTools Performance panel + Lighthouse + RUM for p75 in prod.
Interview framing
"INP is the worst interaction-to-paint latency in a session — target p75 < 200ms. Most wins: break long tasks (< 50ms each), use React 18 transitions / useDeferredValue for non-urgent updates, debounce hot handlers, virtualize lists, move heavy compute to a Worker, and reduce hydration JS via RSC. CSS containment scopes layout work. Always measure via web-vitals in production — synthetic tests miss the real distribution."
Follow-up questions
- •Compare INP vs FID.
- •How does startTransition help?
- •When would you reach for a Web Worker?
Common mistakes
- •Doing heavy work synchronously in event handlers.
- •Not virtualizing long lists.
- •Layout thrash from interleaved read/write.
- •No real-user monitoring — only lab tests.
Performance considerations
- •INP is dominated by main-thread work. Every ms shaved off the longest handler helps. RSC + partial hydration give multi-second wins on hydration-heavy apps.
Edge cases
- •Long input chains (paste 10k chars).
- •Janky devices (low-end Android).
- •Frame budget under battery-saver mode.
Real-world examples
- •Web.dev INP case studies, Next.js partial prerendering, Qwik resumability.