← Back to the library
FrontendApplied · 8 min

Clicks and typing feel laggy

“Typing in a search box drops characters, a button shows no feedback for half a second, and CrUX reports INP above 200 ms, worst on mid-range Android phones.”

THE PRINCIPLE

The browser can only paint between tasks. Show feedback first, then yield, and keep any single task well under 50 ms. INP is good at 200 ms or less and poor above 500 ms.

FIRST MOVES

  1. Collect field INP with web-vitals/attribution: interactionTarget, inputDelay, processingDuration, presentationDelay, and longAnimationFrameEntries name the slow element and scripts.
  2. Reproduce with CPU throttling in the DevTools Performance panel and look for long tasks around the interaction.
  3. High input delay: break up startup work, hydration, and third-party scripts that occupy the main thread.
  4. High processing: update the visible state first, then yield (scheduler.yield where supported, setTimeout fallback) before analytics or heavy work; in React mark expensive updates with useTransition or useDeferredValue.
  5. High presentation delay: shrink DOM size, avoid layout thrashing, and virtualize long lists.

TOOLS: THEN → NOW

First Input Delay (FID)INP, a Core Web Vital that covers every interaction's full latency
Long Tasks APILong Animation Frames (LoAF) for script attribution
setTimeout(0) to yieldscheduler.yield() in Chromium and Firefox, with a setTimeout fallback for Safari

PATTERN SNAPSHOT

// TypeScript's DOM lib does not type scheduler yet, so feature-detect via a cast.
type YieldScheduler = { yield?: () => Promise<void> };
function yieldToMain(): Promise<void> {
  const s = (globalThis as { scheduler?: YieldScheduler }).scheduler;
  if (s?.yield) return s.yield();
  return new Promise((resolve) => setTimeout(resolve, 0));
}

button.addEventListener("click", async () => {
  showSpinner();          // visible feedback in the next frame
  await yieldToMain();
  const result = runFilter(items);
  await yieldToMain();
  render(result);
  await yieldToMain();    // non-urgent work goes last
  sendAnalytics("filter");
});

CLOSE THE AI. EXPLAIN THIS.

An interaction's INP is 600 ms but its event handler takes only 40 ms. Where else can the time be, and what attribution field tells you?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed