Islands and partial hydration
Ship a page as static HTML and send JavaScript only for the few parts that need it, each waking up on its own trigger, so content pages stay fast on slow phones instead of paying to hydrate the whole app.
The problem: paying for a whole app to read an article
Picture a news article page: a header with a menu, a headline, a photo, 1,500 words of text, share buttons, a newsletter signup, comments, and a footer. Most of the page is words and pictures. Only four small parts react to the reader.
The team built the site with a popular framework and server-side rendering (SSR), so the HTML arrives complete and the text shows quickly. Then the page downloads 420 KB of compressed JavaScript. That bundle holds the framework, the router, every component on the page, and the data used to render them, sent a second time as JSON.
420 KB compressed is about 1.5 MB once unzipped, and a mid-range Android phone gets through roughly 1 MB of JavaScript per second of main-thread work (the rule of thumb from the estimation lesson). So the browser spends about 1.5 seconds parsing and running that code, after about two seconds of download on a slow 4G link. Meanwhile the reader taps the menu button and nothing happens. And the headline, the paragraphs, and the footer never needed JavaScript at all.
Why the whole page pays: a quick recap of hydration
Hydration is the step where the framework, running in the browser, walks the server-made HTML, rebuilds its component tree in memory, and attaches event listeners. The key fact: classic hydration works on the whole tree. To attach one handler to the menu button, the framework runs every component, because it cannot know which parts are static without running them.
- Download cost. Every component's code ships, including the many that only print text.
- Data cost. Props for every component ship again as serialized JSON, so the article's text travels twice: once as HTML, once as JSON.
- CPU cost. The framework runs every component in the browser to match it with the existing HTML.
- Waiting cost. Until the whole tree finishes, no part of it responds. A tap during hydration is lost or delayed.
The idea in one sentence
What if the page shipped as plain HTML, and only the four interactive parts got JavaScript, each loaded on its own and started at the moment it is needed? That is the islands architecture, built on partial hydration: hydrating only some parts of a page. A related idea, resumability, tries to skip hydration entirely. This lesson covers both, and what each saves.
Check your understanding · Exercise 1
A server-rendered article page uses classic full hydration. Which of these does the browser pay for, even for the parts of the page that never change? Select all that apply.
The mental model: a static sea with islands
Think of the page as a sea of plain HTML. The sea is made once, on the server or at build time, and it never needs JavaScript. Dotted across it are a few islands: small, self-contained interactive components.
Each island has its own little bundle and starts up on its own. An island knows nothing about the sea around it, and usually nothing about the other islands.
Where the name comes from
Katie Sylor-Miller, then a frontend architect at Etsy, coined the term "component islands" during a 2019 meeting with Jason Miller. In August 2020 Jason Miller, the creator of Preact, wrote the post "Islands Architecture", which gave the pattern its widely used name and description. Server pages with a sprinkle of jQuery were islands in spirit; what was new was doing it with modern components that render on the server and, only where needed, run again in the browser.
Three properties that define an island
- It is its own root. Each island is mounted separately, with its own small component tree. A crash in one island does not take down the others.
- It is server rendered first. The island's HTML is already in the page, so the reader sees a real button, a real form, real comment text, before any JavaScript runs.
- It loads on its own schedule. Each island chooses when to load: right away, when the browser is idle, when it scrolls into view, or when a media query matches. Some never load at all.
What it is not
Islands are not code splitting. Code splitting cuts one app's bundle into chunks, but the app is still one tree and still hydrates as a whole (or at least from its root down). Islands remove the single tree: the static parts have no client code at all, not even lazily loaded code.
Islands are not a client-side app either. Navigation is usually a normal full page load (a multi-page app, or MPA), not a client-side router; the spa-vs-mpa lesson covers that trade-off.
| Question | Full hydration (SSR app) | Islands |
|---|---|---|
| What is the unit of hydration? | The whole page, from the root | Each island, separately |
| What ships for static text? | Its component code and its props | Nothing: it stays plain HTML |
| When does a part become interactive? | When the whole tree has hydrated | When its own trigger fires and its own code has run |
| How do parts share state? | Context, a store, props from the root | Only through something outside the components (a shared store, events, the URL) |
| How does navigation work? | Usually a client-side router | Usually full page loads (MPA) |
Check your understanding · Exercise 2
On a product page, which of these are good candidates to stay as plain static HTML with no island? Select all that apply.
How islands work, step by step
Astro is the best-known islands framework, so we will follow one article page through it. The same steps apply, with different names, in Fresh, Eleventy's is-land, and others. Here is the page. Every component is rendered on the server; only the three with a client: directive become islands.
---
// src/pages/blog/[slug].astro (Astro)
import Header from "../../components/Header.astro";
import MenuButton from "../../components/MenuButton.tsx";
import ShareBar from "../../components/ShareBar.tsx";
import Comments from "../../components/Comments.tsx";
import Footer from "../../components/Footer.astro";
const post = await getPost(Astro.params.slug);
---
<Header>
<MenuButton client:load />
</Header>
<article>
<h1>{post.title}</h1>
<Fragment set:html={post.html} />
</article>
<ShareBar client:idle url={post.url} />
<Comments client:visible postId={post.id} />
<Footer />
One detail surprises people: a React component used without a client: directive is still rendered, but only to HTML. Its code never reaches the browser. The directive, not the component's framework, decides whether something is an island.
The six steps
- Render everything to HTML. At build time or per request, Astro runs every component on the server, including the React ones.
- Wrap each island. Around each component with a directive, Astro writes a wrapper element,
astro-island. Its attributes record where the component's code lives, where the framework's client renderer lives, the props (serialized as JSON), and the directive. - Bundle per island. The bundler builds one small entry chunk per island component. Code shared between islands, like
react-dom, goes into a shared chunk that is downloaded once, however many islands use it. - Paint the HTML. The browser paints the page like any static page; buttons and comments are already real HTML. A tiny inline script defines the
astro-islandelement. - Wait for the trigger. As each
astro-islandelement connects, it reads its directive.loadstarts at once;idlewaits forrequestIdleCallback;visiblesets up anIntersectionObserver;mediachecksmatchMedia. - Hydrate that island alone. When the trigger fires, the element imports the renderer and the component chunk with a dynamic
import(), parses its props, and hydrates its own subtree (for React, ahydrateRootcall on that element). Nothing outside it is touched.
hydrateRoot on its own element.What the HTML looks like
The exact attributes change between Astro versions, so treat this as a simplified picture. Everything needed to wake an island later is written into the HTML now.
<astro-island
uid="Z1abc"
component-url="/_astro/Comments.B7x2.js"
renderer-url="/_astro/client.Dk91.js"
props='{"postId":[0,"p_812"]}'
client="visible"
ssr>
<section class="comments">
<h2>42 comments</h2>
<ul><li>Great article...</li></ul>
<button>Load more</button>
</section>
</astro-island>
Static children inside an island
An island can wrap static content. If you write MenuButton client:load with a list of links inside it, Astro renders those links on the server and passes them into the island as ready-made HTML (Astro calls this a slot). The links' components do not ship. This lets a small interactive shell, like a collapsible panel, hold a large amount of static content without paying for it.
Props are the opposite. Whatever you pass to an island as props is serialized into the HTML and parsed in the browser. Passing a 200-item array to an island that only shows the first five puts all 200 into the page. Props must also be serializable: you cannot pass a function from the server page to an island, because there is no way to write a function into HTML and revive it in the browser.
Check your understanding · Exercise 3
An Astro page has a React ProductReviews component with no client: directive. What does the browser get? Select all that apply.
Choosing when each island wakes: the client directives
The directive answers one question per island: when is it worth spending its bytes and CPU? Astro gives five answers; other tools use similar ones (Eleventy's is-land has on:visible and on:idle).
client:load: as soon as possible
The island's code starts downloading right after the page loads, and it hydrates as soon as the code arrives. Use it for controls the reader is likely to touch in the first seconds and that sit above the fold: a menu button, a search box in the header, a cookie banner's buttons.
The mistake: putting client:load on everything "to be safe". Then every island competes for the network and the main thread at the same moment, and you are back to full hydration with extra steps.
client:idle: when the browser has a free moment
The island waits until the page has finished loading and the browser reports idle time through requestIdleCallback (in browsers without it, Astro waits for the load event instead). Since Astro 4.15 you can pass a timeout, as in client:idle={{ timeout: 500 }}, to cap the wait. Use it for things that should work soon but not first: a share bar, a "like" button, a newsletter box near the top.
The mistake: assuming idle comes quickly. On a busy page on a slow phone, the first idle moment can be seconds away. A control the reader reaches for in the first two seconds should not wait for idle.
client:visible: when it scrolls into view
The island sets up an IntersectionObserver and loads only when its element enters the viewport. Since Astro 4.1 it accepts a rootMargin option, as in client:visible={{ rootMargin: "200px" }}, so it can start a little before it is visible. Use it for anything below the fold: comments, a related-posts carousel, a map, an embedded video player. If the reader never scrolls there, the code is never downloaded.
The mistake: using it for something big and important that the reader reaches quickly. The reader scrolls, sees the comments, taps "Reply", and the 45 KB comments chunk has only just started downloading. Starting early with a rootMargin, or keeping the island small, avoids the gap.
client:media: only when a media query matches
The island loads only if a CSS media query matches, for example client:media="(max-width: 50em)". Use it for parts that only need JavaScript on some screens: a hamburger menu that exists only on mobile, a sidebar filter that becomes a drawer on small screens. On desktop the code is never fetched.
The mistake: hiding the element with CSS on the other screens but forgetting that its server HTML is still there. If the desktop layout shows a different version of the menu, make sure the mobile version is hidden there, or readers of assistive technology may meet two menus.
client:only: skip the server, render in the browser
The component is not rendered on the server at all. The page gets an empty slot, and the component renders from scratch when its code loads, as in client-side rendering. You must name the framework, like client:only="react". Use it for components that cannot run on a server, such as ones that read window or localStorage during render.
The mistake: using it for content. The content is missing from the HTML, so it shows late, may shift the layout when it appears, and is not in the page that search engines and link previews read. Astro lets you pass a fallback (an element with slot="fallback") to reserve the space.
No directive, and server islands
No directive means plain HTML: the default, and the cheapest choice. Astro 5 also added server islands with server:defer: a part of the page, like a personalized "Hi, Maya" box, is rendered on the server in a separate request after the main page, so the main page can be cached on a CDN while the personal part is filled in. It echoes Partial Prerendering from the streaming SSR lesson, with one difference: Partial Prerendering streams the dynamic holes into the same response, while a server island is fetched with its own request.
| Directive | Starts loading | Rendered on the server? | Typical use | Common mistake |
|---|---|---|---|---|
| (none) | Never | Yes | Text, images, links, layout | Forgetting a directive on an interactive component |
client:load | Right after the page loads | Yes | Header menu, search box | Using it everywhere |
client:idle | First idle moment after load | Yes | Share bar, like button | Controls needed in the first seconds |
client:visible | When it enters the viewport | Yes | Comments, carousel, map | Large islands the reader reaches quickly |
client:media | When a media query matches | Yes | Mobile-only menu or drawer | Duplicate menus left in the HTML |
client:only | Right after the page loads | No | Browser-only widgets | Using it for content |
Check your understanding · Exercise 4
Which directive pairings are sensible for a recipe page? Select all that apply.
Partial, progressive, selective: untangling the words
Several terms sound alike and get mixed up, in blog posts and in interviews. They answer different questions. Getting them straight is a quick way to show you understand the topic.
Two questions: which parts, and when
- Partial hydration answers "which parts?": only some components hydrate, and the rest never ship code. Islands are the best-known form. React Server Components are another: server components never ship, client components do.
- Progressive hydration answers "when?": the page still hydrates as one tree, but in pieces over time, ordered by priority or triggered by visibility or interaction, instead of all at once. All the code still ships eventually.
- Selective hydration is React 18's form of progressive hydration. Each
Suspenseboundary hydrates on its own, and React hydrates the boundary the user is interacting with first. The streaming SSR lesson covers it in depth. - Lazy hydration is the informal name for the trigger part: hydrate a component on visibility, idle, or first interaction. Astro's directives are lazy hydration applied to islands. Angular's incremental hydration applies it inside one app tree.
So islands are partial and lazy: only some parts ship code, and each part chooses when. Progressive and selective hydration make full hydration smarter about timing but do not remove code: the static article text is still a component in the tree, still downloaded, still run.
Why the difference matters
Timing techniques make the tapped part respond sooner, which helps INP (Interaction to Next Paint, the Core Web Vital for input responsiveness). But total bytes and total CPU stay about the same, just in smaller slices.
Partial hydration cuts the total. If 80 percent of the page is static, 80 percent of the component code never ships and never runs. That is why content sites see large drops in JavaScript when they move to islands, and why apps that are interactive everywhere see little.
Decision How should a server-rendered page become interactive?
What we pick: For content pages (blogs, docs, marketing, news, product pages) start from islands: most of the page is static, so most of the savings are real. For app-like pages (dashboards, editors, inboxes) where almost everything is interactive, full hydration with selective hydration and code splitting is simpler and loses little. Consider resumability when startup JavaScript is the main problem on a large interactive site and the team can adopt a new framework.
Think first
React Server Components also leave some components out of the client bundle. Are they islands?
They are a form of partial hydration: server components never ship code. But there is still one React tree and one root, with client components as leaves inside it, and navigation can update the tree without a full page load.
Islands have no shared tree at all: each island is a separate root in plain HTML. The react-server-components lesson covers RSC in depth.
Resumability: skipping hydration altogether
Islands reduce how much hydrates. Resumability, the idea behind the Qwik framework (created by Misko Hevery, who earlier created AngularJS), asks a sharper question: why hydrate at all?
The server already ran the components. It knows where every listener is and what every piece of state holds. If it writes that knowledge into the HTML, the browser can resume where the server stopped, instead of running the components again to rediscover it.
What hydration replays
To attach a click handler, a hydrating framework needs two things: which DOM node gets the handler, and the handler function itself, with the state it closes over. Neither is in plain HTML. So the framework calls every component function again in the browser and lines the result up with the DOM. That is the replay: redoing work the server already did, to recover information the server threw away.
Islands shrink the replay to a few components. Resumability tries to remove it. The cost moves from "run components on load" to "write more into the HTML, and fetch small pieces of code when they are needed".
How Qwik resumes
- The compiler splits code at every
$. In Qwik,component$,onClick$, anduseTask$mark boundaries. The Qwik optimizer pulls each marked function into its own tiny chunk with a stable name, so a single click handler can be loaded on its own. - The server writes listeners into attributes. A button's HTML carries an attribute like
on:clickwhose value points at a chunk and a symbol inside it. No code is attached; it is just a string. - The server writes state into the page. The component state and signals the handlers need are serialized into a JSON script block in the HTML.
- The browser loads only the Qwikloader. A small script, about 1 KB, adds one listener per event type on the document. Nothing else runs at startup.
- On the first click, the loader resolves the handler. The event bubbles to the document. The loader walks up from the target, finds the
on:clickattribute, imports that chunk, restores the state it needs, and calls the handler.
<!-- Simplified Qwik output for a counter -->
<button on:click="q-chunk-a1.js#Counter_onClick_x9">
Count: <span>3</span>
</button>
<script type="qwik/json">
{ "objs": [3], "subs": [...] }
</script>
<script id="qwikloader">/* about 1 KB: one listener per event type */</script>
The catch: the first interaction
If the handler's code is fetched only on click, the click waits for a round trip: 100 ms or more on mobile. Qwik handles this by prefetching likely chunks in the background after load (using modulepreload hints or a service worker, depending on version), so the code is usually in the cache by the time the user clicks. That means resumable apps still download code; they just do not execute it at startup.
Other costs are real too. Everything a handler closes over must be serializable, so the optimizer can move it to its own chunk; capturing an instance of an arbitrary class, or an object from a third-party library, in an onClick$ closure fails. Serialized state makes the HTML larger, $ boundaries are a new habit, and the component ecosystem is smaller than React's.
How to talk about its performance
Qwik's headline claim is that startup JavaScript stays near constant as the app grows. That follows from the design. Whether a real product is faster overall depends on prefetching, serialized state size, and how much code the first interactions pull in. Treat it as a property of the architecture, and measure INP and total bytes before promising a number.
| Question | Islands (Astro) | Resumability (Qwik) |
|---|---|---|
| What runs at startup? | Each island's code, when its trigger fires | Only the 1 KB loader |
| Does an island re-run its components? | Yes, it hydrates its own subtree | No, state and listeners are read from the HTML |
| Granularity of loading | Per island (a component and its children) | Per handler or component, split at each $ |
| First interaction | Instant if the island already hydrated | Instant if the chunk was prefetched, else a round trip |
| Mixing frameworks | React, Svelte, Vue, Solid islands on one page | Qwik components (React via an adapter) |
| Learning cost | Low: write normal components | Higher: $ boundaries and serialization rules |
Check your understanding · Exercise 5
Which statements about resumability are true? Select all that apply.
How much JavaScript each approach ships
Numbers make the trade-offs concrete. Take the article page from the first chapter and build it five ways. The sizes below are illustrative but realistic, compressed (gzip or Brotli) as they travel on the wire. To turn them into time we use the two rules of thumb from the estimation lesson: compressed JavaScript is about 3.5 times bigger once unzipped, and a mid-range phone gets through about 1 MB of unzipped JavaScript per second of main-thread work.
The page, priced five ways
- Full hydration: 420 KB, all run at startup: framework 60 KB, router and shell 50 KB, every component 150 KB, shared libraries 100 KB, and the page data again as JSON 60 KB.
- Progressive or selective hydration: still 420 KB. The menu responds sooner, but the total work is the same.
- React islands: 87 KB at startup. React 60 KB, menu 15 KB, share bar 10 KB, loader 2 KB. Comments (45 KB) and signup (15 KB) load only if the reader scrolls to them.
- Preact islands: 32 KB at startup, because the runtime drops to a few KB.
- Resumable (Qwik): about 1 KB runs at startup. Handler chunks are prefetched in the background, but nothing runs until a click.
| Approach | JS at startup | Main-thread work at startup | JS if the reader reaches the end |
|---|---|---|---|
| Full hydration | 420 KB | about 1.5 s | 420 KB |
| Progressive or selective | 420 KB (in slices) | about 1.5 s, in pieces | 420 KB |
| React islands | 87 KB | about 0.3 s | 147 KB |
| Preact islands | 32 KB | about 0.1 s | about 90 KB |
| Resumable (Qwik) | about 1 KB runs | near zero | only handlers that are used, plus prefetch |
Two lessons hide in that table. First, the biggest single saving is not loading the static components, which islands and resumability both achieve. Second, once you use islands, the framework runtime becomes the largest line item, which is why islands sites often pick a small framework (Preact, Svelte, Solid) for their islands.
Lab: decide the islands yourself
Below is the same article page with eight components. For each one, choose: static HTML, or an island with a directive. The lab adds up the JavaScript at startup and in total, estimates when each part becomes interactive on a slow phone, and warns you about the classic mistakes: dead buttons, content missing from the HTML, and early code nobody needed. Try to get startup JavaScript under 100 KB with no warnings.
Startup JavaScript: 222 KB (about 0.8 s of main-thread work). If the reader scrolls to the end: 222 KB. 6 warnings; startup JavaScript 222 KB.
- Reading progress bar reads window during render, so it cannot render on the server: use client:only.
- Headline and article has nothing to click: its 40 KB is wasted.
- Photo carousel is below the fold: client:visible would skip its 25 KB for readers who never get there.
- Newsletter signup is below the fold: client:visible would skip its 15 KB for readers who never get there.
- Comments is below the fold: client:visible would skip its 45 KB for readers who never get there.
- Footer links has nothing to click: its 8 KB is wasted.
Measuring what you ship
- The Network panel, filtered to JS. Add up the scripts after load, then again after scrolling to the bottom: startup cost and worst case.
- The Coverage panel in Chrome DevTools. It shows how much of each loaded script actually ran. Large unused percentages on a content page point at components that should not be islands.
- Total Blocking Time in Lighthouse for the lab view, and INP from real users for the field view. Islands should lower both. If INP does not move, the slow part is probably one heavy island or a third-party script, not hydration.
Check your understanding · Exercise 7
Using the rules of thumb (3.5x unzip, about 1 MB per second on a mid-range phone), which estimates are reasonable? Select all that apply.
Failure modes, edge cases, and accessibility
Islands remove a lot of JavaScript, and with it a lot of bugs. But they add a new kind: the gap between a part of the page that looks ready and a part that is ready. Most failures below come from that gap.
Dead buttons before the island wakes
Server HTML draws a real-looking button long before its island hydrates. If the reader taps it in that window, nothing happens. This is sometimes called the uncanny valley of server rendering, and lazy directives make the window longer. A client:visible menu at the top of the page gains nothing: it is visible at once, so it starts loading at once anyway, just a little later than with client:load.
- Notice it: a high rate of repeated taps on the same control in session recordings, rage-click metrics, or poor INP on first interactions.
- Fix it: use
client:loadfor above-the-fold controls; keep their islands small; and make the HTML work on its own where you can (a menu that is a link to a sitemap page, a form with a realaction). Do not count on a tap made before the island woke being replayed later; test it on a throttled phone.
The forgotten directive
The opposite bug: an interactive component used without a directive renders perfectly and never works. It passes a visual review and a screenshot test. Catch it with an end-to-end test that clicks every control, or a lint rule that flags components with event handlers used without a client: directive.
Layout shift and missing content with client:only
A client:only island has no server HTML, so its space is empty until it renders. When it appears, everything below it moves: a Cumulative Layout Shift (CLS) problem. And its content is absent from the HTML that crawlers and link previews read. Reserve its space with CSS or a fallback slot of the right size, and keep content out of client:only islands.
Heavy props and hydration mismatches
Each island hydrates like a tiny SSR app, with the same mismatch risks: a timestamp in the server's time zone or a random id renders differently in the browser. Big props are the other trap: each island's props are serialized into the HTML, and a page with 30 product-card islands each carrying the full product object can ship more JSON than the old full-hydration page did.
Too many islands, or the wrong page for islands
Islands pay off when interactive parts are few and far apart. A search results page where every card has a favorite button, a hover preview, and a compare checkbox becomes 60 islands. At that point one island around the list, or a regular hydrated app, is simpler and often faster.
Navigation is the other edge. Islands sites are usually multi-page: every link is a full page load, and every island starts from scratch. A music player island stops when the reader clicks to the next article. Astro's client router with transition:persist can keep an island alive, but needing a lot of that is a sign you want a single-page app.
Accessibility angles
- Start from semantic HTML. Because the server HTML is what users meet first, it must already be correct: real
buttonandaelements, labels on inputs, headings in order. An island should add behavior, not fix markup. - State attributes must be right before hydration. A disclosure rendered with
aria-expanded="false"that does not respond yet is confusing but honest. One rendered without the attribute, which the island adds later, announces differently before and after. - Focus must survive hydration. Hydration that reuses the DOM keeps a keyboard user's focus; a
client:onlyisland that replaces a placeholder can lose it. - Live regions belong in the server HTML. A cart badge that announces changes needs its
aria-liveregion present from the start; a region inserted at hydration time may not be announced.
Check your understanding · Exercise 8
A news site moved to islands. Field data shows readers tapping the header menu, a client:idle island, two or three times before it opens. Which fixes address the cause? Select all that apply.
In real products, and in the interview
Who uses which approach
- Astro is islands-first: zero JavaScript by default, with islands in React, Preact, Svelte, Vue, or Solid. It is popular for docs (its Starlight theme), blogs, and marketing sites.
- Fresh, Deno's framework, ships only the Preact components in its
islandsfolder. - Eleventy offers
is-land, a small custom element that wakes its children on triggers like visible, idle, or a media query, around any framework or plain web components. - Marko, the framework eBay built and uses for its pages, has done automatic partial hydration for years: its compiler works out which components are interactive, so authors do not mark islands by hand.
- Qwik, built at Builder.io, is the main resumable framework, with Qwik City for routing.
- React Server Components (Next.js App Router and others) bring partial hydration into a single React tree. The react-server-components lesson covers them.
Where you do not see islands: Gmail, Figma, Notion, trading dashboards. Almost every pixel is interactive and the user stays for hours, so the startup cost is paid once and amortized. The choosing-rendering lesson walks through those cases.
When to bring it up
Bring islands up when the prompt is content-heavy and reached from search or links: a news site, a blog platform, docs, a product detail page, a landing page. Those prompts usually list SEO and fast first load on mobile as non-functional requirements. That is your cue: "most of this page is static, so I would ship it as HTML and hydrate only the interactive parts".
Do not force it into an app prompt. For a chat app, a spreadsheet, or a dashboard, say why islands do not fit: almost everything is interactive and shares state, so the savings are small and the glue code is large.
What to draw and what to say
- Draw the page and shade the islands, each labelled with its trigger.
- Give a number. "Full hydration would ship around 400 KB and keep a mid-range phone busy for about 1.5 s. Islands put about 90 KB on the critical path, about 0.3 s."
- Name the cost. Shared state across islands (cart badge and add-to-cart via a tiny store plus a cookie), full page navigations, and the dead-button window for lazy islands.
- Mention the alternatives. Selective hydration for app-like React pages, Server Components for partial hydration in one tree, resumability when startup JavaScript is still the bottleneck.
How deep to go by level
| Level | What a strong answer shows |
|---|---|
| Junior | Knows hydration ships code for static parts; can say "only hydrate the interactive parts" and name Astro. |
| Mid | Picks a directive per component with reasons; explains partial versus progressive hydration; estimates bytes. |
| Senior | Designs shared state between islands, handles the dead-button window and navigation, compares islands, RSC, and resumability, and says when not to use islands. |
| Staff | Sets a JavaScript budget per page type, adds lint and CI checks (bundle size, forgotten directives), and plans a gradual migration route by route. |
Check your understanding · Exercise 9
Which prompts are good fits for an islands architecture? Select all that apply.
Summary
- Classic hydration re-runs every component in the browser, so a mostly static page pays to download and run code for text that never changes.
- Islands render the page as static HTML and hydrate only the interactive components, each as its own root, with its own bundle and its own trigger.
- Choose a trigger per island:
client:loadfor early, above-the-fold controls;client:idlefor soon-but-not-first;client:visiblefor below the fold;client:mediafor one screen size;client:onlyfor browser-only widgets, never for content. - Partial hydration reduces how much runs; progressive and selective hydration only change when it runs. Only the first lowers the total cost.
- Resumability (Qwik) skips the replay by writing listeners and state into the HTML and loading each handler on demand; the costs are serialization rules and a possible first-interaction delay without prefetching.
- Islands share no tree, so shared state needs a tiny store, events, or a cookie, and each extra framework adds a runtime.
- Price it with numbers: 3.5x unzip and about 1 s per MB on a mid-range phone turns 420 KB of full hydration into about 1.5 s, and 87 KB of islands into about 0.3 s.
- Islands fit content pages; app-like pages that are interactive everywhere gain little and are better served by a hydrated app or Server Components.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
What is the islands architecture, and why does it help content pages?
The page is rendered to static HTML on the server or at build time, and only the interactive components, the islands, ship JavaScript. Each island is its own small root with its own bundle and hydrates on its own trigger: at load, when idle, when visible, or when a media query matches.
On a content page most components only print text, so classic full hydration spends most of its bytes and CPU re-running components that never change. Islands cut that to zero. For an article page that can mean going from around 400 KB of startup JavaScript to under 100 KB, roughly 1.5 s of main-thread work on a mid-range phone down to about 0.3 s.
Question 2
How is partial hydration different from progressive or selective hydration?
Partial hydration decides which parts hydrate: static parts never ship code. Progressive hydration decides when: the whole tree still hydrates, but in pieces, by priority or trigger. Selective hydration is React 18's version, where Suspense boundaries hydrate independently and the one being interacted with goes first.
Progressive and selective hydration improve responsiveness during load but keep the total bytes and CPU about the same. Partial hydration lowers the total. Islands combine both ideas: partial, plus a trigger per island.
Question 3
Explain resumability. What does Qwik avoid, and what does it pay instead?
Hydration replays components in the browser to find out where listeners go and to rebuild state. Qwik avoids that replay: the server writes each listener as an attribute pointing at a code chunk, and serializes the needed state into the HTML. A 1 KB loader listens at the document level and, on an event, imports just that handler and runs it with the restored state.
It pays with a new programming model (code split at every $, captured values must be serializable), larger HTML for the state, and a possible delay on the first interaction if the handler was not prefetched. Startup JavaScript stays tiny by design; real-world gains should be measured rather than assumed.
Question 4
Two islands, an add-to-cart button and a header cart badge, must stay in sync. How?
They are separate roots, so a context provider will not work. I would put the count in a tiny shared store module that both islands import; the bundler puts it in one shared chunk, so they share one instance, even across frameworks. Each island reads the current value when it hydrates, then subscribes.
Because islands sites use full page loads, I would also keep the count in a cookie, so the server renders the right number in the badge's HTML and nothing flickers. If many islands need lots of shared state, I would question whether the page should be islands at all.
Question 5
Which client directive would you give a header menu, a share bar, a comments section, and a mobile-only filter drawer? Why?
Header menu: client:load, because it is above the fold and used early, and a lazy trigger would widen the dead-button window. Share bar: client:idle, useful soon but not first. Comments: client:visible, most readers never reach them. Filter drawer: client:media with a small-screen query, so desktop never fetches it.
Everything else, the article, images, related links, and footer, stays plain HTML with no directive.
Question 6
When would you not use islands?
When almost everything on the page is interactive and shares state, like an editor, a dashboard, or a chat app. Islands would become one big island or dozens of small ones glued with shared stores, and the savings would be small. Also when client-side navigation that keeps state across pages is a core requirement, like a music player that must keep playing.
There, a fully hydrated app with code splitting and selective hydration, or React Server Components, is simpler and loses little.
Practice it
These design problems lean on islands and partial hydration.
References
- Islands Architecture (Jason Miller, 2020) The post that named and described the pattern.
- Astro docs: Islands architecture
- Astro docs: Template directives reference Every client: directive and its options.
- Astro docs: Server islands
- Qwik docs: Resumable vs. hydration
- Rendering on the Web (web.dev) How hydration, streaming, and partial rendering compare.
- 11ty is-land A framework-free islands custom element.
Read next
- Server-side renderingThe server builds the full HTML for each request, so users see content before any JavaScript runs, then the browser hydrates it: faster first paint and better SEO, paid for with server work, a later first byte, and a second render in the browser.
- Streaming server renderingThe server sends a page's HTML in pieces as each part becomes ready, instead of waiting for its slowest data: the layout paints right away, slow sections stream in later, and each piece hydrates on its own.
- React Server ComponentsReact components that run only on the server and send their rendered result, not their code, to the browser: data fetching moves into components, heavy libraries stay off the client, and only components below a
"use client"boundary ship JavaScript. - Choosing a rendering strategyHow to pick the rendering strategy for each page by asking who sees it, how often it changes, how interactive it is, and whether strangers must find it, then mixing strategies route by route in one app instead of choosing one for everything.
- Code splitting and bundlingBreaking one big JavaScript bundle into smaller pieces that load only when needed, because every kilobyte of JavaScript costs download, parse, and run time on the user's device.
- Core Web VitalsThree numbers, measured on real users, that say whether a page loads fast (LCP), reacts fast (INP), and stays still (CLS): what each one measures, what makes it bad, and how to fix it.