Modern architecture with a field of flowers in the foreground. Hero image credit: Photo by Guohua Song on Pexels

Part 14 of the Nuxt and .NET series.

Part 13 identified the complaint: the document page looks finished while the revision switcher ignores clicks. The cause is that hydration has not reached it yet. The obvious response — hydrate less, later — is right for some components and actively harmful for others, and this article is about telling them apart.

The examples are from the fictional document application. No timings are given, because they depend on the page, the device, the network and the cache state.

Table of Contents

Two Components With Opposite Requirements

The document page contains, among other things:

  • the revision switcher, at the top, next to the title. It is the reason people open the page, and a visitor may click it the moment they see it.
  • the change-volume chart, well below the fold, which visualizes how much the document changed between revisions. It is nice to have, it is the heaviest component on the page by dependency weight, and most visits never scroll to it.

Hydrating both eagerly means the chart’s code competes with the switcher’s during startup, on a device that may have little capacity to spare. Deferring both means the switcher is inert exactly when it is most likely to be used. Neither uniform policy is defensible, which is the whole argument for treating hydration as a per-component decision.

Visible Is Not Interactive

Between the moment server-rendered HTML paints and the moment components respond, there is a window in which the page is a picture of an application.

flowchart LR A["SSR HTML arrives"] --> B["Content is visible"] B --> C["Client modules load"] C --> D["Vue hydrates"] D --> E["Controls respond"] B -. "this window is the problem" .- E

It is worth being precise about what that window is not. It is not Total Blocking Time, which measures blocking portions of long main-thread tasks inside a lab window. A page can have very little blocking work and a long unusable window, simply because the code that would make a control work has not been requested yet. Optimizing for the metric and optimizing for the window are different activities, and only one of them is what the user noticed.

One thing works in your favour: not everything needs hydration to function. A server-rendered anchor navigates, a plain form submits, and a <details> element opens. Controls whose behaviour is entirely JavaScript — a switcher that swaps content in place, a filter that re-queries — have nothing to fall back on, so they need either progressive enhancement (render them as something that works without JavaScript and enhance afterwards), prompt hydration, or at minimum an honest loading state so the interface does not claim readiness it lacks.

Choosing a Trigger per Component

Frameworks increasingly support declaring when a component hydrates, and the useful triggers map onto recognizable component roles:

Trigger Suitable for Risk if misapplied
Eager (default) Primary controls, anything above the fold a visitor may use immediately Startup contention when overused
On visibility Below-the-fold charts, carousels, maps A fast scroller arrives before it is ready
On idle Secondary widgets that should work soon but not first Idle may not come on a busy page
On media query Components only present at some viewports Resize behaviour needs testing
On interaction Rarely used heavy components, e.g. an editor behind a button The first interaction pays the load, so it needs feedback
flowchart TB Q1{"Can a visitor plausibly<br/>use it in the first seconds?"} -->|Yes| EAGER["Hydrate eagerly"] Q1 -->|No| Q2{"Is it off-screen?"} Q2 -->|Yes| VIS["Hydrate on visibility"] Q2 -->|No| Q3{"Is it heavy and rarely used?"} Q3 -->|Yes| INT["Hydrate on interaction,<br/>with visible feedback"] Q3 -->|No| IDLE["Hydrate when idle"]

Whatever the trigger, two contracts must survive it. The component’s state must be the state the server rendered — a deferred component still hydrates against existing markup, so Part 12’s rule applies unchanged, and a deferred component that recomputes a value is a mismatch that happens later and is harder to catch. And its accessibility behaviour must be intact when it arrives: a control that manages focus, announces changes, or implements keyboard semantics in JavaScript is not accessible while it waits, so “on interaction” for such a component needs care that the interaction reaching it is not swallowed.

It is also worth stating what is not available. Vue does not provide a directive that excludes an arbitrary subtree from hydration, so a project-invented hydrate-never is a custom renderer integration that needs its own documentation and tests — and anything left permanently unhydrated is inert markup, which is only acceptable for content that genuinely has no behaviour.

What an Idle Callback Promises

requestIdleCallback is the trigger most often misread, so it is worth separating what it does from what people hope.

function scheduleOptionalWork(work, timeoutMs) {
  if ('requestIdleCallback' in window) {
    window.requestIdleCallback(work, { timeout: timeoutMs })
  } else {
    window.setTimeout(work, timeoutMs)
  }
}

It schedules work for a moment when the browser has spare capacity, and its timeout is a scheduling constraint — “run by then even if not idle” — not a guarantee that anything is ready by then. The fallback above does not detect idleness at all; it just waits. And once the callback fires, the downloading, parsing, module evaluation and hydration still have to happen, on a main thread that may be busy with whatever made it busy in the first place.

Related mechanism, related misunderstanding: resolving a promise queues a microtask, which runs before the browser gets an opportunity to paint. It is not a yield. If a long task needs to give the browser a chance to respond, it needs a real task boundary or a supported scheduling API — and the fallback needs testing rather than assumption.

Preload Hints Interact With the Choice

Deferring hydration and deferring downloads are different things, and preload hints are where they meet.

<!-- Works: the stylesheet is fetched but not applied -->
<link rel="stylesheet" href="print.css" media="print">

<!-- Does not work: media is ignored, the module is preloaded anyway -->
<link rel="modulepreload" href="/_nuxt/entry.js" media="none">

media="none" is not a scheduling mechanism for modules. Attempts to defer downloads this way produce a page that behaves exactly as before, plus a line of code everyone later assumes is doing something.

The inverse mistake is removing preload hints to make a waterfall look shorter. The hints exist so the browser can discover dependencies early; remove them and discovery happens as modules are parsed, which can lengthen the import waterfall and delay the very readiness you were optimizing. If an experiment removes them, inspect the production network waterfall afterwards and confirm which other links or imports still pull the same modules — fewer early requests is not the goal, and it is easy to mistake for one.

Deferring the Whole Application

Beyond per-component triggers lies the invasive option: rewrite the generated entry script so the application itself starts on a trigger. It can reduce early contention substantially, and it is a long-term maintenance commitment with a specific checklist:

  • generated entry names and output shapes change with framework upgrades;
  • script type, integrity, content-security-policy nonce and other attributes must be preserved;
  • the trigger must be idempotent, so two events cannot start two startups;
  • listeners and timers must be cleaned up after firing;
  • existing navigation and resource hints must keep working;
  • a failed entry download needs a defined outcome;
  • and user actions occurring before listeners exist need an answer.

That last point is the one that decides whether this technique is acceptable on a given page. A click that arrives while the entry script is still downloading is not automatically replayed as the intended action later. Framework-supported interaction hydration may provide replay for the boundaries it owns; a hand-written entry loader inherits no such guarantee, and a button that swallows the first click is a worse experience than one that was simply a little slow.

One non-negotiable: do not relax the content security policy to make an injected loader work. Use the application’s established nonce or external-script mechanism, and verify against a production build rather than the dev server.

What to Measure

Because the goal is a usable interface rather than a number, the evaluation is mostly a scenario list:

Scenario What must hold
Pointer or touch input immediately after paint The intended action happens, or is visibly queued
Keyboard navigation before hydration Focus order and activation remain sensible
Slow connection Nothing looks ready while silently ignoring input
Busy main thread Idle triggers and their timeouts still produce acceptable behaviour
Navigation before hydration completes No duplicate startup, no stale state
Client script fails entirely Server-rendered content remains useful
Repeated audit runs The application behaves as it does for ordinary visitors

Alongside that, two quantitative comparisons are worth keeping: early main-thread work, and the time from paint to the first successful interaction with the page’s primary control. The second is the one that corresponds to the original complaint, and it is the one an audit score does not contain.

Report results as what they are. “Less early main-thread work, with interaction readiness for secondary components moved later” is supportable. “Removed unused JavaScript” usually is not, because deferred work is not removed work — it is the same work at a different moment, which is a trade and should be described as one.

The Two Components Resolved

The switcher hydrates eagerly. It is the page’s purpose, a visitor may click it immediately, and its behaviour exists only in JavaScript, so nothing else is defensible. If its hydration is slow, the fix is to make it lighter — fewer dependencies, less work in setup — not to make it later.

The chart hydrates on visibility. It is below the fold, most visits never reach it, and if a fast scroller arrives before it is ready, a placeholder with its known dimensions keeps the layout stable and tells the truth about what is happening.

The generalizable part is the question, not the two answers: for each interactive component, what happens if a user acts on it in the first second? If the answer is “the action works,” it may wait. If the answer is “nothing happens and the page gives no sign,” it may not — regardless of what deferring it would do to any metric. Hydration scheduling is worth doing deliberately and component by component; it is not worth doing to move work outside a measurement window.

What’s Next


Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.