Event Loop and Latency Engineering
Why this chapter matters
- Input responsiveness is now a first-class user experience concern.
- Expensive JavaScript, rendering work, and synchronous handlers can make a visually loaded page feel broken.
- Senior frontend engineers must reason about main-thread contention, not only bundle size.
Core mental model
The browser main thread coordinates JavaScript execution, style calculation, layout, paint, and much of input handling. When JavaScript runs too long, the browser cannot quickly respond to user input. A page can look ready and still feel slow because the main thread is busy.
Latency engineering is the practice of keeping critical interactions short, interruptible, and observable.
Common sources of input delay:
- large hydration work after initial HTML arrives
- expensive event handlers
- synchronous validation or formatting on every keypress
- rendering huge tables or charts in one task
- JSON parsing or data transformation on the main thread
- third-party scripts competing with product code
Decision framework
| Decision | Option A | Option B | Tradeoff signal |
|---|---|---|---|
| Heavy computation | Main thread | Worker | Use workers when computation blocks input or paint |
| Rendering data | Render all rows | Virtualize/paginate | Use virtualization when DOM size dominates interaction cost |
| Validation | On every keystroke | Debounced/on blur | Choose by feedback need and handler cost |
| Hydration | Hydrate full page | Island/deferred hydration | Defer non-critical interactivity |
Implementation patterns
- Measure first: use performance profiles, long-task data, and interaction telemetry.
- Split work: chunk expensive tasks so the browser can handle input between them.
- Move work: send CPU-heavy computation to workers where practical.
- Reduce work: avoid unnecessary re-renders, oversized DOM, and repeated parsing.
- Prioritize work: hydrate and render what the user needs first.
Atlas example
Atlas report filters update a chart and table. If every filter change synchronously recalculates chart series, formats thousands of rows, and re-renders the whole table, INP will suffer. A senior design separates the path:
- update the selected filter immediately
- show an "updating" state without blocking input
- cancel or ignore stale requests
- virtualize large tables
- memoize stable transforms
- move heavy aggregation to the server or worker
Anti-patterns and failure modes
- Loaded but frozen: the route paints, then hydration blocks input. Prevention: profile hydration and defer non-critical islands.
- Keystroke tax: every input runs expensive validation and filtering. Prevention: debounce and scope validation.
- Table explosion: thousands of DOM nodes make every interaction expensive. Prevention: paginate or virtualize.
- Third-party blindness: analytics scripts hurt interaction latency. Prevention: budget and monitor third-party execution.
- Optimization by guess: teams change code without profiling. Prevention: capture before/after traces.
Verification checklist
- Critical interactions have latency targets.
- Long tasks are tracked during the user journey.
- Heavy transforms are memoized, moved, or split.
- Large lists are paginated or virtualized.
- Third-party script cost is visible.
Metrics and scorecards
Leading indicators:
- long tasks over 50ms during critical flows
- handler duration for key interactions
- hydration duration by route
- render count for expensive components
Lagging indicators:
- p75 INP by route and device class
- user abandonment after interaction
- support reports of frozen or laggy UI
Exercises
- Profile a slow interaction and identify the top three main-thread costs.
- Redesign an expensive filter interaction so the visible control updates before heavy work.
- Decide whether a data transform belongs on the server, in a worker, or on the main thread.