Topics
v6.0.0
On this page · sections

Long Tasks Observer

A long task is any main-thread task exceeding 50ms — it blocks input, delays paint, and directly hurts INP. The Long Tasks API lets you observe and attribute them in production.

ConceptThresholdImpact
Long task> 50ms on main threadBlocks input and next paint
Total Blocking TimeSum of long task excessLab metric (Lighthouse)
INPWorst interaction latencyField metric tied to long tasks
LoAFLong animation framesAttribution — which script

What Counts as a Long Task

Definition

The browser reports tasks on the main thread that exceed 50ms. During a long task:

  • Input events queue (input delay)
  • requestAnimationFrame callbacks delay
  • Paint and composite stall
User click → Input queued (waiting for long task) → Long task finishes (80ms) → Event handler runs → Style/layout/paint → Next frame — INP measured

Common sources: large React re-renders, JSON.parse of megabytes, synchronous layout reads, third-party scripts, hydration.

Total Blocking Time (TBT) in Lighthouse sums (duration - 50ms) for all long tasks during load. INP captures the worst interaction in field — long tasks are the usual root cause.

Interview: 50ms is the RAIL threshold — one frame budget at 60fps is ~16ms; 50ms is perceptible delay.

PerformanceObserver for Long Tasks

Definition

const longTaskObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.duration > 50) { sendToAnalytics({ name: 'long-task', duration: entry.duration, startTime: entry.startTime, attribution: entry.attribution?.map((a) => ({ name: a.name, containerType: a.containerType, containerSrc: a.containerSrc, })), }); } } }); longTaskObserver.observe({ type: 'longtask', buffered: true });

entry.attribution (where supported) links to script URL or container (iframe, embed). Buffer buffered: true to catch tasks before observer registration.

Send beacons with route, device memory, and connection type for segmentation. Sample 10–30% on high-traffic pages.

Interview: Register observer early with buffered: true. Attribute to script URL via attribution array.

Diagnosing Jank in React SPAs

Definition

Workflow:

  1. Reproduce with Performance panel — enable Screenshots + Memory
  2. Find red triangles (long tasks) in Main thread
  3. Expand call stack — identify React commit, parse, or third-party
  4. Correlate with route change or WebSocket burst
Long task during route change → Check: large sync render? heavy useMemo on mount? → Fix: code-split route, startTransition, lazy mount Long task during scroll → Check: unvirtualized list? layout reads in handler? → Fix: virtualize, passive listeners, rAF batching Long task from third-party → Check: attribution.containerSrc → Fix: defer load, facade pattern, remove

React DevTools Profiler + Performance panel together: Profiler shows which components; Performance shows main-thread blocking time.

Interview: Profile first — don’t memo everything blindly. Long tasks during hydration mean code-split or SSR slimmer shell.

Breaking Up Work Without Workers

Definition

When workers aren’t viable (need DOM access), yield to the main thread:

async function processInChunks(items) { const CHUNK = 200; for (let i = 0; i < items.length; i++) { processItem(items[i]); if (i % CHUNK === 0 && scheduler?.yield) { await scheduler.yield(); } } } function legacyChunk(items) { let i = 0; function run(deadline) { while (i < items.length && deadline.timeRemaining() > 0) { processItem(items[i++]); } if (i < items.length) requestIdleCallback(run); } requestIdleCallback(run); }

Other techniques:

  • startTransition — mark non-urgent React updates
  • requestAnimationFrame batching — one DOM write pass per frame
  • Debounced input — reduce handler frequency
  • scheduler.postTask — prioritized scheduling (emerging)

setTimeout(0) yields a macrotask but still blocks until complete — prefer scheduler.yield().

Interview: Chunk work to keep tasks under 50ms. Yield every N iterations in hot loops.

Observability Pipeline

Definition

Staff-level long-task RUM pipeline:

PerformanceObserver (longtask) → Beacon with duration + attribution + route → Aggregate p75/p95 by route and release → Alert on regression vs baseline → Tie to INP RUM for same sessions

Dashboard metrics:

  • Long tasks per pageview (p75)
  • Total long-task time per session
  • Top attribution URLs (first-party vs third-party)
  • Correlation: long task count vs INP rating

CI: synthetic test with CPU throttle — fail if TBT exceeds budget on critical routes. Pair with LoAF for frame-level attribution.

Governance: third-party script approval requires long-task budget. Feature flags for heavy client features — monitor long-task spike on rollout.

Interview: Connect long-task RUM to INP and release versioning. Alert on p95 regression, not just averages.

Senior Interview Summary

Mental model

Main thread task > 50ms → long task Blocks input, rAF, paint → hurts INP Observe with PerformanceObserver type=longtask Fix: chunk, yield, worker, or reduce work

Practical rules

  • Register longtask observer early with buffered
  • Attribute via entry.attribution — first vs third party
  • Keep tasks under 50ms — yield every ~200 iterations
  • Virtualize lists; code-split routes; defer third-party
  • CI perf budget on TBT for critical routes

What to say in interviews

“I’d wire a PerformanceObserver for longtask events in RUM, send duration and attribution URL, and correlate with INP by route. In DevTools I’d find the slowest interaction, expand the long task stack, and fix the root cause — usually an unchunked loop, heavy hydration, or third-party script. I’d use scheduler.yield or startTransition to break up remaining work.”

Common traps

  • Assuming rAF alone fixes jank — heavy compute in rAF still blocks
  • Workers for DOM-touching work — not possible
  • Measuring only Lighthouse TBT — field INP is the contract
  • Ignoring third-party long tasks in attribution

Last updated on