PageSourceSearch

https://hoist-learn.netlify.app/chunk-BrBK9gXD.js

js hoist-learn.netlify.app collected 2026-10-03 10:53:34 UTC 15,009 bytes, 58 lines download raw bytes

1import{t as e}from"./chunk-zqjrRax5.js";var a={id:`islands-resumability`,moduleId:`rendering`,trackId:`platform-2026`,title:`Islands & resumability`,blurb:`Astro, Qwik, and the case against hydration.`,minutes:11,level:`deep`,tldr:{model:`Hydration re-executes every component to rebuild a tree the server already built. Islands pay that for a marked few; resumability serialises state and wiring into the HTML and continues instead of replaying.`,gotcha:`Neither is free: islands are separate roots so they cannot easily share reactive state, and resumability just moves the work to first interaction, betting on prefetch to hide the round trip.`},blocks:[{kind:`prose`,md:`The previous lesson was about *where code runs*. This one is about a narrower and older complaint: even once the server has produced perfect HTML, the browser insists on doing an enormous amount of work before that HTML will respond to a click.
2
3That work is **hydration**, and two architectures exist because people decided it was not worth paying. **Islands** pays it for a few components instead of all of them. **Resumability** tries not to pay it at all.
4
5Both are documented architecture rather than something a unit test can demonstrate, so this lesson names real APIs and real mechanisms and states no benchmark numbers \u2014 the honest version of this comparison is about mechanism and shape, and vendor benchmarks in this area are chosen to flatter.`},{kind:`heading`,text:`What hydration actually costs`},{kind:`prose`,md:`Hydration is not "attaching event listeners", which is how it is usually described and why it sounds cheap. To attach a listener to the right node the framework must first know what the tree *is*, and the server did not send it that \u2014 the server sent HTML. So the client has to:
6
71. **Download** the component code.
82. **Parse and compile** it \u2014 main-thread work proportional to raw, uncompressed bytes.
93. **Re-execute every component** to rebuild the framework's tree in memory, including all the props, state and closures the server already built once.
104. **Walk that tree**, attach listeners, and reconcile it against the existing DOM.
11
12Step 3 is the one that stings. The server rendered the page; the client renders it again to obtain the same answer, purely so that the framework has an in-memory model to update later. The markup was on screen the whole time.
13
14And the cost scales with the **number of interactive components in the application**, not with what the user can see. A page of text with a search box pays for the whole framework, plus every component in the tree the framework needs to walk.`},{kind:`callout`,tone:`gotcha`,title:`The uncanny valley`,md:`The window between first paint and the end of hydration is worse than a slow page, because the page **looks finished**. Buttons are visible and styled; nothing indicates waiting. A user clicks, and either nothing happens or the click sits queued behind a long task and fires half a second later.
15
16In the field this is early-interaction **INP** and **Total Blocking Time**. It is also why "we improved LCP" can coincide with users complaining that the site got worse: painting sooner widens the valley if hydration did not get faster too.`},{kind:`heading`,text:`Islands: make interactivity opt-in`},{kind:`prose`,md:`The islands architecture \u2014 the term is Katie Sylor-Miller's, popularised by Jason Miller, and **Astro** is its best-known implementation \u2014 starts from an observation about real pages: most of a document is static. Prose, headings, images, navigation, footers. Only a handful of regions ever respond to anything.
17
18So: render the entire page to static HTML on the server, then hydrate **only the components explicitly marked interactive**. Each is an independent root \u2014 its own bundle, its own hydration, no shared parent tree \u2014 floating in a sea of inert markup. Everything unmarked ships zero JavaScript, forever.
19
20This inverts the SPA default. In a normal app interactivity is a property of the whole tree and you claw back bundle size afterwards with code splitting. With islands, interactivity is opt-in per component, and the static majority is protected by construction.
21
22The other half of Astro's design is that islands are **framework-agnostic** \u2014 a React island and a Svelte island can sit on the same page, because they were never part of one tree to begin with.`},{kind:`code`,lang:`html`,caption:`Astro: the page is static, the directives mark the islands`,code:`---
23import Search from '../components/Search.jsx';
24import Cart   from '../components/Cart.svelte';
25const posts = await getPosts();     // runs at build or request time only
26---
27
28<article>
29  <h1>{posts[0].title}</h1>
30  <p>{posts[0].body}</p>          <!-- zero JS, forever -->
31</article>
32
33<Search client:idle />            <!-- hydrate when the browser is idle -->
34<Cart   client:visible />         <!-- hydrate when scrolled into view -->
35<Chart  client:media="(min-width: 60rem)" />`},{kind:`prose`,md:"The directives are the API worth remembering:\n\n- **`client:load`** — hydrate immediately. For anything above the fold that must work at once.\n- **`client:idle`** — hydrate on `requestIdleCallback`. The sensible default for secondary widgets.\n- **`client:visible`** — hydrate on an `IntersectionObserver` intersection. The best-value option for anything below the fold, because most users never scroll to most of it.\n- **`client:media`** — hydrate only if a media query matches, so a de
35sktop-only widget costs a phone nothing.\n- **`client:only`** — skip server rendering altogether.\n\n`client:only` is the trap. It emits no HTML, so the island is a blank space until its JavaScript arrives and runs — precisely the failure the architecture exists to avoid. It is a legitimate escape hatch for a component that genuinely cannot render without `window`, and a bug everywhere else.\n\nThe limitation to state honestly: **islands do not compose.** Two islands cannot share reactive state directly, because they are separate roots with no common ancestor. You coordinate through URL state, a store outside the framework, custom events, or by promoting the shared parent to a single larger island. On a page that is mostly static that constraint costs nothing; on an app where everything talks to everything, you are fighting the model."},{kind:`heading`,text:`Resumability: do not replay, continue`},{kind:`prose`,md:`**Qwik** takes the more radical position: the problem is not that hydration runs for too many components, it is that hydration exists.
36
37The insight is that the server already did all this work \u2014 built the tree, computed the state, worked out which handler belongs to which node \u2014 and then threw the result away, sending only markup. Resumability keeps it. **The state and the wiring are serialised into the HTML**, so the client can pick up exactly where the server stopped.
38
39The word to have ready in an interview: hydration **replays** what the server did; resumability **continues** it.
40
41Concretely, an event listener is not attached at startup. It is an attribute holding a URL:`},{kind:`code`,lang:`html`,caption:`Qwik output — the wiring lives in the markup`,code:`<button on:click="./chunk-a.js#Counter_click[0]">Add</button>
42
43<script type="qwik/json">
44  { "objs": [ \u2026 application state \u2026 ],
45    "subs": [ \u2026 which signal feeds which node \u2026 ] }
46<\/script>`},{kind:`prose`,md:"At startup, one tiny global listener is installed — a few kilobytes, and no component code at all. When the button is clicked it reads the attribute, fetches `chunk-a.js`, and invokes the export. **The first component code to execute is the one you interacted with.**\n\nThe serialised blob does more than hold data: it encodes the **reactivity subscription graph** (which signal is read by which DOM node) along with object identity and cycles. That is why the framework can update the right nodes after the handler runs, despite never having built the graph in the browser.\n\nThe consequence is that startup cost is roughly **constant with application size**. A ten-page app and a ten-thousand-component app both boot by installing one listener. Qwik's compiler is doing the heavy lifting — it splits every handler, every `useTask$`, every component into its own lazy chunk, which is what the `$` suffix in the API marks: a **lazy-loading boundary**, not decorative syntax."},{kind:`compare`,aLabel:`Islands (Astro)`,bLabel:`Resumability (Qwik)`,rows:[{label:`Core idea`,a:`Hydrate a few components, not all`,b:`Serialise state and wiring; never hydrate`},{label:`JS at startup`,a:`Zero, until an island activates`,b:`A small global listener, always`},{label:`Startup scales with`,a:`Number and size of islands`,b:`Roughly nothing`},{label:`First interaction`,a:`Instant if already hydrated`,b:`May fetch a chunk — a round trip if not prefetched`},{label:`HTML size`,a:`Normal`,b:`Larger — state and wiring travel with it`},{label:`Shared state across regions`,a:`Awkward: separate roots`,b:`Native: one serialised graph`},{label:`Ecosystem`,a:`Any framework, per island`,b:`Qwik-specific, much smaller`},{label:`Best fit`,a:`Content sites with interactive widgets`,b:`Large apps needing fast cold first load`}]},{kind:`callout`,tone:`warn`,title:`Resumability is a trade, not a free win`,md:`The costs are specific. **HTML gets bigger**, because the state and subscription graph ship inside it. **Everything must serialise**, which constrains what may live in state and is why ordinary functions and closures need framework-specific wrappers — the \`$\` boundary is a real constraint on how you write code, not sugar. **Many small chunks** means the first interaction can cost a network round trip on a cold cache, which is why Qwik prefetches aggressively via a service worker; if that prefetch misses, the metric you damaged is INP.
47
48So resumability does not delete the work. It **moves it from startup to first interaction**, and then bets on prefetching to hide it. That is usually a good bet — most startup work is for interactions that never happen — but it is a bet, and it is fair to say so.`},{kind:`heading`,text:`Choosing, and where this app sits`},{kind:`prose`,md:`The rule underneath all of it: **the more of your page is genuinely static, the more these architectures pay.**
49
50A marketing or documentation site is 95% prose and 5% widgets \u2014 islands are close to free money. Commerce is a good resumability case: anonymous visitors on cold caches, first load matters commercially, and pages are large. A trading dashboard or an editor is interactive everywhere, so there is no static majority to protect, the session lasts an hour, and a conventional SPA is the right answer \u2014 startup cost amortises to nothing.
51
52Hoist is that third category, and deliberately so. It is a client-rendered PWA because it must work **offline**, which rules out an architecture whose first move is asking a server for HTML. But it applies the same underlying principle \u2014 *do not load what this screen does not need* \u2014 through the levers an SPA has: **every route lazily loaded**, **every one of the 108 lesson bodies behind its own dynamic import**, a service worker that prefetches, and about **92 kB of initial transfer**.
53
54That is worth noticing. The islands directive \`client:visible\` and Angular's \`@defer (on viewport)\` are the same idea reached from opposite directions: one starts from static HTML and adds interactivity, the other starts from an app and subtracts what is not needed yet. Neither architecture owns the insight.`},{kind:`callout`,tone:`interview`,title:`How this gets asked`,md:`Either *"what is wrong with hydration?"* or *"when would you reach for Astro or Qwik?"* \u2014 and the failure mode is answering with a preference instead of a mechanism.
55
56Start with the cost, in four steps: download, parse and compile, **re-execute every component to rebuild a tree the server already built**, then walk it and attach listeners. Emphasise step three; that is the part most people leave out, and it is the part that makes hydration expensive rather than merely fiddly. Name the **uncanny valley** \u2014 the page looks ready and ignores clicks \u2014 and connect it to INP and TBT so it lands as a measured problem rather than an aesthetic one.
57
58Then the two answers. **Islands** make interactivity opt-in per component, each its own root, so a static page ships nothing; the cost is that islands cannot easily share state. **Resumability** serialises the state and the listener wiring into the HTML so the client continues rather than replays, making startup roughly constant; the cost is bigger HTML, a serialisation constraint on how you write code, and a first interaction that may fetch a chunk. Finish by choosing on page shape \u2014 mostly static, islands; interactive everywhere, a normal SPA \u2014 and by pointing out that lazy routes and viewport-deferred blocks give an SPA most of the same benefit without the architecture change.`},{kind:`takeaways`,items:[`Hydration costs four things: download, parse and compile, re-execute every component to rebuild the tree, then walk it and attach listeners.`,`The expensive step is re-execution — the client recomputes a tree the server already built, only to have something to update later.`,`Hydration scales with the number of interactive components in the app, not with what the user can see.`,`The uncanny valley is the gap between paint and interactivity; it shows up as early-interaction INP and TBT, and a faster paint widens it.`,`Islands render the page statically and hydrate only marked components, each an independent root — interactivity becomes opt-in.`,`Astro directives: client:load, client:idle, client:visible, client:media, and client:only, which skips server HTML and is usually a bug.`,`Islands cannot easily share reactive state, because they are separate roots with no common ancestor.`,`Resumability serialises application state and listener wiring into the HTML so the client continues instead of replaying.`,`A Qwik listener is an attribute holding a chunk URL; a tiny global handler fetches it on demand, so no component code runs at startup.`,`The serialised blob carries the subscription graph plus object identity and cycles, which is why updates work without rebuilding anything.`,`Resumability costs larger HTML, a serialisation constraint on state and closures, and a possible round trip on first interaction.`,`It moves work from startup to first interaction and bets on prefetching to hide it — usually a good bet, still a bet.`,`Choose on page shape: mostly static favours islands, interactive-everywhere favours a normal SPA, large cold-load-critical apps favour resumability.`,`This app is a client-rendered PWA because it must work offline, and gets the same benefit from lazy routes, per-lesson dynamic imports and a prefetching service worker.`]}
58],cards:e};export{a as ISLANDS_RESUMABILITY};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.