Broader Ideas

Where this module's mechanism reappears later in the programme, and what it explains.

Hydration is the canonical long task

Server-rendered HTML paints quickly (good LCP), then hydration attaches handlers and rebuilds client state — classically as one uninterruptible task. The result is a page that looks interactive and is not, which is the single most common cause of a good-LCP/bad-INP profile.

Every mitigation is a task-decomposition strategy, and this module lets you evaluate them from first principles rather than by reputation:

ApproachWhat it does to the task graph
Progressive / selective hydrationsplits one long task into many, prioritised by interaction likelihood
Islandseliminates hydration for subtrees that never needed it
Server Componentsmoves component work off the client entirely; nothing to hydrate
Streaming SSRoverlaps server work with client parsing; does not by itself shorten hydration

The last row is the one teams get wrong: streaming improves time-to-first-byte-of-content and does nothing for hydration cost. If your dominant INP term is proc, streaming will not move it. Picked up properly in fe-21-rendering-architectures.

React's scheduler as a worked example

packages/scheduler/src/forks/Scheduler.js — read schedulePerformWorkUntilDeadline. It prefers MessageChannel over both microtasks and setTimeout, for exactly the reasons measured here: a microtask-based scheduler would have better total throughput and be useless for responsiveness, and setTimeout is clamped and queued behind everything. It is the tasks-versus-microtasks asymmetry, chosen deliberately by people who had to live with the consequences.

Worth reading in source rather than in a blog post, and worth logging in ../fe-00-roadmap/docs/learning-log.md §3. Followed up in fe-09 (Fiber, render vs commit) and fe-12 (concurrent rendering as scheduling).

Debounce is not a race fix

A debounced input reduces the number of in-flight requests. It changes nothing about ordering. If request N returns after request N+1, the user sees results for a query they already replaced — the "stale response overwrites newer data" incident from the specification's case-study list.

The three fixes are not equivalent, and ranking them is the seam into fe-17:

FixGuaranteesFails when
AbortController cancellationrequest is actually cancelled client-sideserver already committed a side effect
Sequence number / last-write-winscorrect renderingwasted work; server still processed both
Render-time guard (compare response to current state)correct rendering, simplestwasted work; no cancellation signal upstream

For a read, any of the three works and the ranking is about efficiency. For a request with a server-side effect, only cancellation is safe, and even then only if the server honours it — which is why idempotency keys exist and why this becomes a distributed-systems conversation in fe-18.

Scheduling as an accessibility concern

Long tasks are not neutral across users. Focus movement queues behind the blocked main thread, so a keyboard user pressing Tab gets no response and — unlike a mouse user — has no hover feedback confirming the app is alive. Screen-reader users additionally suffer from aria-live regions updated per chunk, which floods the announcement queue. The correct pattern is announcing start and completion, not progress.

prefers-reduced-motion interacts here too: a rAF-driven busy animation must be suppressible, which means the jank canary from this module's labs is a debugging tool, not a production spinner. Developed in fe-24-accessibility.

The compositor as an architectural boundary

The red square in starvation.html keeps spinning through a completely blocked main thread, because it animates transform on the compositor. This is worth generalising: "the page looks alive" and "the page is responsive" are independent claims, and most loading indicators assert the first while users care about the second.

The corollary is the threading rule behind "animate transform, not left" — it is not a style preference, it is which thread owns the animation. And it is why a single non-passive wheel or touchstart listener hands your scroll performance to your worst long task: the compositor must ask the main thread whether preventDefault() will be called. Picked up in fe-11 and fe-27-mobile-web.

Scheduling shows up in test infrastructure

Experiment B has a direct consequence for testing: .click() and fireEvent do not interleave microtasks between listeners, while real user input does. A test suite driving UI synthetically has different semantics from production. If any listener's correctness depends on another's promise having settled, the suite is structurally incapable of catching it.

The mitigation is knowing which of your tests use real input (Playwright's page.click() goes through CDP and is trusted) versus synthetic dispatch (Testing Library's fireEvent; userEvent is closer but still synthetic). Developed in fe-32 and fe-33.

Scheduling as an SLO

Once you can decompose INP, you can argue about which metric an organisation should commit to. INP is user-centric but lagging and sparse; Total Blocking Time is lab-measurable and leading but does not reflect real interaction patterns. The mature position is to pair them: a field metric for truth and a lab metric for CI gating, with the explicit acknowledgement that the lab metric is a proxy. Developed in fe-43-observability and fe-44-reliability.