Module 03 advanced 40 min

Interactivity — INP & TBT

Long tasks, LoAF, yielding with scheduler.yield, debouncing, workers, and hydration strategies.

  • INP
  • TBT

INP is the responsiveness metric. Poor INP almost always means the main thread was busy when the user tried to interact.

INP mechanics

For each tap/click/keypress, the browser measures until the next frame is presented after handlers finish:

  1. Input delay — thread busy; event queued
  2. Processing time — your listeners + framework work
  3. Presentation delay — style/layout/paint before next frame

INP = p98 of interaction durations (worst interactions dominate).

One interaction Input delay Processing Present INP = input delay + processing + presentation delay (worst interaction p98)

Long tasks & LoAF

Tasks > 50 ms are “long.” They block input and increment TBT in lab traces.

Long Animation Frames (LoAF) — newer API surfacing scripts inside slow frames:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LoAF', entry.duration, entry.scripts);
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

Use Chrome DevTools Performance panel: bottom-up view → sort by Self time → find your bundles.

The 50 ms slicing heuristic

DurationStrategy
< 50 msRun synchronously
50–250 msSlice + scheduler.yield()
> 250 msWeb Worker or off main thread
async function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    return await scheduler.yield();
  }
  return new Promise((r) => setTimeout(r, 0));
}

async function processLargeList(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 50 === 0) await yieldToMain();
  }
}

INP Lab — blocking loop vs scheduler.yield()

Click a button — Event Timing will report interaction latency.

scheduler.postTask priorities

scheduler.postTask(() => updateCriticalUI(), { priority: 'user-blocking' });
scheduler.postTask(() => syncAnalytics(), { priority: 'background' });

Priorities: user-blockinguser-visiblebackground.

Debounce & throttle

PatternUse when
DebounceSearch input — run after pause
ThrottleScroll/resize — max once per frame/window
rAF throttleVisual scroll effects — sync to paint
function debounce(fn, ms) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}

Web Workers

Offload parsing, compression, heavy transforms:

const worker = new Worker('/worker.js', { type: 'module' });
worker.postMessage({ data: largePayload });
worker.onmessage = (e) => updateUI(e.data);

Workers don’t touch DOM — postMessage back for UI updates (still schedule those updates thoughtfully).

Framework hydration & INP

Hydration = attaching client listeners to server HTML. Full-page hydration on load competes with first interactions.

StrategyINP impact
Islands / partial hydrationOnly interactive regions hydrate
client:visible / client:idleDefer hydration until needed
Resumability (advanced)Serialize state; less re-execution
Event delegationFewer listeners

DON’T hydrate entire dashboards on landing if only one widget is interactive.

Third-party scripts

Analytics, chat, ads — defer and self-host when possible:

<script defer src="https://cdn.example/analytics.js"></script>

Audit with Performance panel → Third-party lane. Remove or delay anything not needed for first interaction.

INP debug workflow

  1. RUM attribution — which element/event?
  2. Local trace — reproduce interaction, find long task
  3. Fix: yield, worker, defer hydration, or remove work
  4. Verify p98 in RUM after deploy

Next: Module 04 — CLS and visual stability.

Live on this page

TTFB
FCP
LCP
INP
CLS