Skip to main content

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

DecisionOption AOption BTradeoff signal
Heavy computationMain threadWorkerUse workers when computation blocks input or paint
Rendering dataRender all rowsVirtualize/paginateUse virtualization when DOM size dominates interaction cost
ValidationOn every keystrokeDebounced/on blurChoose by feedback need and handler cost
HydrationHydrate full pageIsland/deferred hydrationDefer non-critical interactivity

Implementation patterns

  1. Measure first: use performance profiles, long-task data, and interaction telemetry.
  2. Split work: chunk expensive tasks so the browser can handle input between them.
  3. Move work: send CPU-heavy computation to workers where practical.
  4. Reduce work: avoid unnecessary re-renders, oversized DOM, and repeated parsing.
  5. 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.