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.
| Concept | Threshold | Impact |
|---|---|---|
| Long task | > 50ms on main thread | Blocks input and next paint |
| Total Blocking Time | Sum of long task excess | Lab metric (Lighthouse) |
| INP | Worst interaction latency | Field metric tied to long tasks |
| LoAF | Long animation frames | Attribution — 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)
requestAnimationFramecallbacks 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 measuredCommon 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 viaattributionarray.
Diagnosing Jank in React SPAs
Definition
Workflow:
- Reproduce with Performance panel — enable Screenshots + Memory
- Find red triangles (long tasks) in Main thread
- Expand call stack — identify React commit, parse, or third-party
- 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, removeReact 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 updatesrequestAnimationFramebatching — 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 sessionsDashboard 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 workPractical 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