{"id":20,"date":"2026-06-06T21:48:43","date_gmt":"2026-06-06T21:48:43","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=20"},"modified":"2026-06-06T23:45:07","modified_gmt":"2026-06-06T23:45:07","slug":"deferred-hydration-done-right-the-requestidlecallback-trick-and-the-modulepreload-pitfall","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=20","title":{"rendered":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Fourteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Hydration Dilemma<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SSR gives you a fast first paint: complete HTML that the browser can render immediately. Then the framework\u2019s JavaScript arrives, gets parsed, compiled, and executed to <strong>hydrate<\/strong> the page\u2014wiring up event listeners and making everything interactive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">During hydration, the main thread is busy. Buttons do not respond. Forms do not accept input. This gap between \u201clooks ready\u201d and \u201cis ready\u201d is <strong>Total Blocking Time (TBT)<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If SSR has already rendered the page, why rush to download and execute JavaScript before the user even finishes the first paragraph?<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  A[SSR Server] --&gt; B[\"HTML Response&lt;br\/&gt;Fully rendered markup\"]\n  B --&gt; C[Browser parses HTML]\n  C --&gt; D[\"First Paint \/ FCP&lt;br\/&gt;Looks ready\"]\n  D --&gt; E[\"Hydration JS downloads&lt;br\/&gt;parse &amp; execute\"]\n  E --&gt; F[\"Event listeners attached&lt;br\/&gt;Interactive\"]\n\n  classDef paint fill:#e0f7fa,stroke:#006064;\n  classDef js fill:#fff3e0,stroke:#e65100;\n  classDef interactive fill:#e8f5e9,stroke:#1b5e20;\n\n  class B,C,D paint\n  class E js\n  class F interactive<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Naive Approach That Does Not Work<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The obvious idea: delay JavaScript modules with the <code>media<\/code> attribute, just like you can with stylesheets:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;!-- This works for stylesheets: --&gt;\n&lt;link rel=\"stylesheet\" href=\"print.css\" media=\"print\"&gt;\n&lt;!-- Browser downloads but doesn't apply until print --&gt;\n\n&lt;!-- This does NOT work for modulepreload: --&gt;\n&lt;link rel=\"modulepreload\" href=\"\/_nuxt\/entry.js\" media=\"none\"&gt;\n&lt;!-- Browser IGNORES the media attribute and preloads anyway --&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The spec does not define <code>media<\/code> semantics for <code><link rel=\"modulepreload\"><\/code>, so browsers ignore it. Module scripts are preloaded eagerly regardless.[7]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is poorly documented. You only notice when performance metrics refuse to improve, even after you add <code>media=\"none\"<\/code> to 90 <code>modulepreload<\/code> links.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Stylesheet preload\n    S1[\"&lt;link rel=#quot;stylesheet#quot;&lt;br\/&gt;media=#quot;print#quot;&gt;\"] --&gt; S2[\"Browser may delay applying&lt;br\/&gt;until media matches\"]\n  end\n\n  subgraph Modulepreload\n    M1[\"&lt;link rel=#quot;modulepreload#quot;&lt;br\/&gt;media=#quot;none#quot;&gt;\"] --&gt; M2[\"Browser still preloads&lt;br\/&gt;module eagerly\"]\n  end<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Working Approach: Complete Removal<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Because <code>modulepreload<\/code> hints cannot be meaningfully delayed, the solution is to <strong>remove them entirely<\/strong>.[7] A Nitro <code>render:html<\/code> plugin performs two transformations in a large SSR application.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Transformation 1: Remove All Modulepreload Links<\/h3>\n\n\n\n<pre><code class=\"language-txt\">Before (standard Nuxt SSR output):\n  &lt;head&gt;\n    &lt;link rel=\"modulepreload\" href=\"\/_nuxt\/entry.js\"&gt;\n    &lt;link rel=\"modulepreload\" href=\"\/_nuxt\/chunk-abc.js\"&gt;\n    &lt;link rel=\"modulepreload\" href=\"\/_nuxt\/chunk-def.js\"&gt;\n    &lt;link rel=\"modulepreload\" href=\"\/_nuxt\/chunk-ghi.js\"&gt;\n    ... (90+ more)\n  &lt;\/head&gt;\n\nAfter (deferred):\n  &lt;head&gt;\n    &lt;!-- All modulepreload links removed --&gt;\n  &lt;\/head&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Without these hints, the browser no longer speculatively downloads chunks. It waits until it encounters actual <code><script><\/code> tags.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  subgraph Before: Standard Nuxt SSR\n    BHead[\"&lt;head&gt;\"] --&gt; B1[entry.js modulepreload]\n    BHead --&gt; B2[chunk-abc.js modulepreload]\n    BHead --&gt; B3[chunk-def.js modulepreload]\n    BHead --&gt; B4[chunk-ghi.js modulepreload]\n    BHead --&gt; Bn[... 90+ more hints]\n  end\n\n  subgraph After: Deferred\n    AHead[\"&lt;head&gt;\"] --&gt; Acomment[\"&lt;!-- All modulepreload links removed --&gt;\"]\n  end<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Transformation 2: Replace the Entry Script<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">By default, the Nuxt entry script loads immediately:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;!-- Standard: loads and executes immediately --&gt;\n&lt;script type=\"module\" src=\"\/_nuxt\/entry.js\"&gt;&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The replacement defers it via <code>requestIdleCallback<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-html\">&lt;script&gt;\n  function load() {\n    var s = document.createElement('script')\n    s.type = 'module'\n    s.src = '\/_nuxt\/entry.js'\n    document.head.appendChild(s)\n  }\n  if ('requestIdleCallback' in window) {\n    requestIdleCallback(load, { timeout: 3000 })\n  } else {\n    setTimeout(load, 50)\n  }\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>requestIdleCallback<\/code> tells the browser: \u201cRun this function when you\u2019re idle between frames.\u201d The <code>{ timeout: 3000 }<\/code> guard ensures the script still loads within 3 seconds, even if the browser never becomes truly idle.[5]<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  A[Page load] --&gt; B{requestIdleCallback&lt;br\/&gt;supported?}\n  B --&gt;|Yes| C[\"Schedule load() with&lt;br\/&gt;requestIdleCallback&lt;br\/&gt;timeout: 3000ms\"]\n  B --&gt;|No| D[\"Fallback to&lt;br\/&gt;setTimeout(load, 50)\"]\n\n  C --&gt; E[\"Create &lt;script type='module'&gt;\"]\n  D --&gt; E\n  E --&gt; F[\"Append to &lt;head&gt;\"]\n  F --&gt; G[Browser downloads \/_nuxt\/entry.js]\n  G --&gt; H[Hydration starts]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What Happens at Load Time<\/h2>\n\n\n\n<pre class=\"mermaid\">timeline\n  title Deferred Hydration Timeline\n  0ms    : HTML arrives, browser starts parsing\n  50ms   : HTML fully parsed, no JS to block\n  80ms   : First Contentful Paint (FCP) \u2014 user sees complete page content\n  80-300ms : Browser mostly idle while user is reading\n  300ms  : requestIdleCallback fires, browser starts downloading entry.js\n  500ms  : entry.js downloaded and parsed; it imports route-level chunks via import()\n  700ms  : Route chunk downloaded; Vue hydration begins\n  800ms  : Hydration complete \u2014 page fully interactive<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Compare this to the standard Nuxt flow:<\/p>\n\n\n\n<pre class=\"mermaid\">timeline\n  title Standard Hydration Timeline\n  0ms    : HTML arrives, browser starts parsing\n  50ms   : Browser discovers ~90 modulepreload hints and starts downloading all chunks\n  50-400ms : Network saturated downloading JS; main thread busy parsing\/compiling; HTML rendering delayed\n  400ms  : First Contentful Paint (FCP) \u2014 user finally sees content\n  600ms  : All chunks downloaded, hydration begins\n  800ms  : Hydration complete<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The numbers above are representative for a large enterprise application with many route-level chunks; exact timings depend on bundle size and network conditions.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Measured Impact<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On a representative complex SSR application (Nuxt 3\/4, core + multiple feature modules) in Lighthouse mobile mode with simulated 4G throttling:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Metric<\/th><th>Before<\/th><th>After<\/th><th>Reason<\/th><\/tr><\/thead><tbody><tr><td><strong>FCP<\/strong><\/td><td>~2.5 s<\/td><td>< 1.0 s<\/td><td>No JS blocking HTML paint<\/td><\/tr><tr><td><strong>Speed Index<\/strong><\/td><td>~3.5 s<\/td><td>< 1.5 s<\/td><td>Visual content renders without waiting for JS<\/td><\/tr><tr><td><strong>TBT<\/strong><\/td><td>~500 ms<\/td><td>< 100 ms<\/td><td>Main thread free during initial load<\/td><\/tr><tr><td><strong>Unused JS<\/strong><\/td><td>Flagged<\/td><td>Eliminated<\/td><td>Chunks only load when the entry bundle imports them<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">These improvements align with what generic delayed-hydration techniques for Nuxt aim to achieve.[3][5]<\/p>\n\n\n\n<pre class=\"mermaid\">graph LR\n  subgraph Before\n    FCPb[FCP ~2.5s]\n    SIb[Speed Index ~3.5s]\n    TBTb[TBT ~500ms]\n    UJSb[Unused JS flagged]\n  end\n\n  subgraph After\n    FCPa[\"FCP &lt;1.0s\"]\n    SIa[\"Speed Index &lt;1.5s\"]\n    TBTa[\"TBT &lt;100ms\"]\n    UJSa[Unused JS eliminated]\n  end\n\n  FCPb --&gt; FCPa\n  SIb --&gt; SIa\n  TBTb --&gt; TBTa\n  UJSb --&gt; UJSa<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Only Works with SSR<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This technique assumes the server already sent fully rendered HTML. Without SSR, removing hints and deferring the entry script just yields a <strong>blank page<\/strong>.[4][6]<\/p>\n\n\n\n<pre><code class=\"language-txt\">With SSR + Deferred Hydration:\n  Browser receives: &lt;html&gt;...&lt;h1&gt;Welcome&lt;\/h1&gt;...&lt;section&gt;...&lt;\/section&gt;...&lt;\/html&gt;\n  Browser paints: Complete page visible \u2713\n  JS loads later: Hydration adds interactivity\n\nWithout SSR + Deferred Hydration:\n  Browser receives: &lt;html&gt;&lt;body&gt;&lt;div id=\"app\"&gt;&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\n  Browser paints: Blank page \u2717\n  JS loads later: Page eventually renders, but user saw nothing<\/code><\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph SSR + Deferred\n    S1[\"Server sends&lt;br\/&gt;fully rendered HTML\"] --&gt; S2[\"Browser paints&lt;br\/&gt;complete page\"]\n    S2 --&gt; S3[Deferred JS loads] --&gt; S4[\"Hydration adds&lt;br\/&gt;interactivity\"]\n  end\n\n  subgraph No SSR + Deferred\n    C1[\"Server sends&lt;br\/&gt;&amp;lt;div id=#quot;app#quot;&amp;gt; only\"] --&gt; C2[\"Browser paints&lt;br\/&gt;blank shell\"]\n    C2 --&gt; C3[Deferred JS loads] --&gt; C4[\"Client-side render&lt;br\/&gt;content finally appears\"]\n  end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It is an SSR-only strategy. You trade a brief delay in interactivity for instant visual completeness\u2014but only when the HTML already contains the content.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Selective Hydration: The <code>hydrate-never<\/code> Directive<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Deferred hydration determines <em>when<\/em> JavaScript runs. Selective hydration determines <em>how much<\/em> JavaScript runs.[4][6]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Static text, decorative images, and structural containers do not change on the client. Hydrating them is wasted work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A custom directive marks subtrees to skip (patterned after Nuxt\u2019s lazy-hydration work like <code>v-hydrate-never<\/code>).[4][6]<\/p>\n\n\n\n<pre><code class=\"language-txt\">Usage:\n  &lt;div v-hydrate-never&gt;\n    &lt;h2&gt;About Our Company&lt;\/h2&gt;\n    &lt;p&gt;Founded in 1999, we have been serving customers...&lt;\/p&gt;\n    &lt;p&gt;Our mission is to provide...&lt;\/p&gt;\n    &lt;!-- 500 words of static content --&gt;\n  &lt;\/div&gt;\n\nEffect:\n  Vue's hydration skips this entire subtree:\n  \u2022 No patch() calls\n  \u2022 No reactive tracking setup\n  \u2022 No vnode creation for children\n  \u2022 Zero TBT contribution from this section<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On pages with large static sections (landing pages, editorial content, legal text), selective hydration can cut TBT by 30\u201350% in practice. The server already rendered the markup\u2014Vue does not need to re-verify or instrument it.[4][6]<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  R[Root Vue app] --&gt; D1[\"Dynamic section&lt;br\/&gt;v-hydrate\"]\n  R --&gt; D2[\"Static section&lt;br\/&gt;v-hydrate-never\"]\n\n  D1 --&gt; H1[\"Hydration work:&lt;br\/&gt;patch(), reactivity,&lt;br\/&gt;vnode creation\"]\n  D2 --&gt; H2[Skipped by hydration]\n\n  classDef heavy fill:#ffebee,stroke:#c62828;\n  classDef light fill:#e8f5e9,stroke:#2e7d32;\n\n  class H1 heavy\n  class H2 light<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Interaction with Manual Chunking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With <code>modulepreload<\/code> removed, the browser only discovers route-level chunks when <code>entry.js<\/code> executes <code>import()<\/code> calls. That creates a deliberate, staged sequence:<\/p>\n\n\n\n<pre><code class=\"language-txt\">Loading Sequence (deferred + manual chunks):\n\n  1. requestIdleCallback fires\n  2. entry.js loads (core framework)\n  3. entry.js calls import('.\/route-homepage.js')\n  4. route-homepage.js loads (page-specific code)\n  5. route-homepage.js calls import('.\/cms-components.js')\n  6. cms-components.js loads (CMS section renderers)\n\n  Each import() creates a new network request.\n  But by this point, the page is already visible.\n  These requests happen AFTER first paint,\n  when the browser has bandwidth available.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Manual chunking ensures chunks are <strong>large enough<\/strong> to avoid a waterfall of tiny requests yet <strong>small enough<\/strong> to avoid shipping large amounts of unused code.<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n  participant B as Browser\n  participant E as entry.js\n  participant RH as route-homepage.js\n  participant CMS as cms-components.js\n\n  B-&gt;&gt;E: Download &amp; execute (after idle)\n  E-&gt;&gt;RH: import('.\/route-homepage.js')\n  B-&gt;&gt;RH: Fetch route-homepage chunk\n  RH--&gt;&gt;E: Module loaded\n  RH-&gt;&gt;CMS: import('.\/cms-components.js')\n  B-&gt;&gt;CMS: Fetch CMS components chunk\n  CMS--&gt;&gt;RH: Module loaded\n  RH-&gt;&gt;B: Start route hydration<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Edge Cases and Fallbacks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">No <code>requestIdleCallback<\/code> Support<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Safari only added <code>requestIdleCallback<\/code> in version 16.4 (2023). Older browsers fall back to <code>setTimeout(load, 50)<\/code>, which gives the browser a moment to paint but cannot wait for true idle time. Users may see a brief period where the page is visible but not yet interactive.[5]<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Timeout for Busy Pages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The 3-second timeout ensures the callback still fires if the browser never declares itself idle. Without it, a constantly busy page might never hydrate.[5]<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  A[Page starts busy] --&gt; B[\"requestIdleCallback scheduled&lt;br\/&gt;with timeout=3000\"]\n  B --&gt; C{Browser becomes idle&lt;br\/&gt;before 3000ms?}\n  C --&gt;|Yes| D[\"Callback runs at idle&lt;br\/&gt;load entry.js\"]\n  C --&gt;|No| E[\"Timeout reached at 3000ms&lt;br\/&gt;callback forced to run\"]\n  D --&gt; F[Hydration proceeds]\n  E --&gt; F<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Server-Side Bot Detection<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For crawlers and synthetic monitoring (e.g., Lighthouse), server-side logic can detect bots. Bot traffic bypasses deferred hydration entirely\u2014crawlers need content, not interactivity.[3][5]<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  R[Incoming request] --&gt; D{Is bot \/ crawler?}\n  D --&gt;|Yes| Bypass[\"Serve standard SSR&lt;br\/&gt;no deferred hydration\"]\n  D --&gt;|No| Defer[\"Serve SSR + deferred&lt;br\/&gt;requestIdleCallback loader\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><code>modulepreload<\/code> is not controllable \u2014 only removable<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No attributes like <code>media<\/code>, <code>loading<\/code>, or <code>fetchpriority<\/code> currently affect <code>modulepreload<\/code>. To control when chunks download, you must remove these hints and rely on <code>import()<\/code> timing.[7] This is a limitation of the spec and browser implementations, not of any specific framework.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>requestIdleCallback<\/code> is the right abstraction for \u201cnot now\u201d<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Unlike <code>setTimeout(fn, 0)<\/code> (next macrotask) or <code>requestAnimationFrame<\/code> (just before the next paint), <code>requestIdleCallback<\/code> fires when the browser has spare capacity. It is designed exactly for \u201cdo this later, but not at the user\u2019s expense.\u201d[5]<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  A[Work to schedule] --&gt; B{Urgency?}\n  B --&gt;|Run ASAP| C[\"setTimeout(fn, 0)&lt;br\/&gt;next macrotask\"]\n  B --&gt;|Before next paint| D[requestAnimationFrame]\n  B --&gt;|When idle| E[\"requestIdleCallback&lt;br\/&gt;with optional timeout\"]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Deferred hydration and selective hydration are complementary<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Deferred hydration controls <strong>when<\/strong> hydration runs (after idle). Selective hydration controls <strong>what<\/strong> actually hydrates (skip static subtrees).[4][6] Used together, they minimize TBT\u2014even on low-end mobile hardware.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  SSR[SSR-rendered page] --&gt; DH[\"Deferred hydration&lt;br\/&gt;via requestIdleCallback\"]\n  SSR --&gt; SH[\"Selective hydration&lt;br\/&gt;v-hydrate-never\"]\n\n  DH --&gt; TBT1[Reduced blocking from timing]\n  SH --&gt; TBT2[Reduced blocking from volume]\n\n  TBT1 --&gt; TBT[Low Total Blocking Time]\n  TBT2 --&gt; TBT<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">This technique is SSR-specific and opinionated<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For a short period, users interact with a \u201cdead\u201d page. Click handlers and other events do not work until hydration completes. On content-heavy pages, this is effectively invisible\u2014users are reading, not clicking. On highly interactive pages (dashboards, editors, complex tools), deferred hydration may be the wrong trade-off.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Series Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This wraps up the 27-article series on building a modern web application with Nuxt 4, GraphQL, and cloud-native infrastructure:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Architecture<\/strong>: Schema stitching, modular design, CMS integration  <\/li>\n<li><strong>Automation<\/strong>: Code generation, type-safe forms, scaffolding  <\/li>\n<li><strong>Performance<\/strong>: SSR, caching, deferred hydration, image proxying  <\/li>\n<li><strong>Operations<\/strong>: Containerized runtime, process managers, observability, security  <\/li>\n<li><strong>Developer Experience<\/strong>: AI-assisted debugging, structured logging, framework modules  <\/li>\n<li><strong>Advanced Patterns<\/strong>: A\/B testing, conditional rendering, drill-down filters  <\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The common thread: <strong>generate what can be generated, measure what you assume, remove what does not add value.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Fourteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Dilemma SSR gives you a fast first paint: complete HTML that the browser can render immediately. Then the framework\u2019s JavaScript arrives, gets parsed, compiled, and executed to hydrate the page\u2014wiring up event listeners and making everything interactive. During [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":236,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-20","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=20\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production\" \/>\n<meta property=\"og:description\" content=\"Fourteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Dilemma SSR gives you a fast first paint: complete HTML that the browser can render immediately. Then the framework\u2019s JavaScript arrives, gets parsed, compiled, and executed to hydrate the page\u2014wiring up event listeners and making everything interactive. During [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=20\" \/>\n<meta property=\"og:site_name\" content=\"Scalable Web Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:48:43+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-06T23:45:07+00:00\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall\",\"datePublished\":\"2026-06-06T21:48:43+00:00\",\"dateModified\":\"2026-06-06T23:45:07+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20\"},\"wordCount\":938,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/14-deferred-hydration-done-right.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20\",\"name\":\"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/14-deferred-hydration-done-right.jpg\",\"datePublished\":\"2026-06-06T21:48:43+00:00\",\"dateModified\":\"2026-06-06T23:45:07+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=20\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/14-deferred-hydration-done-right.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/14-deferred-hydration-done-right.jpg\",\"width\":1880,\"height\":1235},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=20#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=20","og_locale":"en_US","og_type":"article","og_title":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production","og_description":"Fourteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Hydration Dilemma SSR gives you a fast first paint: complete HTML that the browser can render immediately. Then the framework\u2019s JavaScript arrives, gets parsed, compiled, and executed to hydrate the page\u2014wiring up event listeners and making everything interactive. During [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=20","og_site_name":"Scalable Web Production","article_published_time":"2026-06-06T21:48:43+00:00","article_modified_time":"2026-06-06T23:45:07+00:00","author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=20#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=20"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall","datePublished":"2026-06-06T21:48:43+00:00","dateModified":"2026-06-06T23:45:07+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=20"},"wordCount":938,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=20#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/14-deferred-hydration-done-right.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=20#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=20","url":"https:\/\/softwareproduction.eu\/?p=20","name":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall - Scalable Web Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=20#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=20#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/14-deferred-hydration-done-right.jpg","datePublished":"2026-06-06T21:48:43+00:00","dateModified":"2026-06-06T23:45:07+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=20#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=20"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=20#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/14-deferred-hydration-done-right.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/14-deferred-hydration-done-right.jpg","width":1880,"height":1235},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=20#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"Deferred Hydration Done Right \u2014 The `requestIdleCallback` Trick and the `modulepreload` Pitfall"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/14-deferred-hydration-done-right.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/20","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=20"}],"version-history":[{"count":8,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/20\/revisions"}],"predecessor-version":[{"id":237,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/20\/revisions\/237"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/236"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=20"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=20"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=20"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}