Rendering Pattern

Islands Architecture

tl;dr: An “islands” page is a static HTML document with small, independent zones of interactivity dropped into it. Each interactive zone ships its own JavaScript and hydrates on its own schedule; the rest of the page stays plain HTML. The result is a page that loads close to instantly because most of it has nothing to hydrate.

Single-page apps got us into the habit of shipping a JavaScript runtime, a router, a state manager, and the components for an entire view - even for pages that are 90% prose. Islands architecture is the answer to “what if we only paid for the interactive parts?“. The term was coined by Katie Sylor-Miller and popularized by Jason Miller in 2020, and over the last few years it has gone from a curiosity to a first-class option supported by Astro, Fresh, Marko 6, Enhance, and others.

This article walks through the model, contrasts it with the alternatives that arrived alongside it (React Server Components, Qwik’s resumability), and finishes with the current framework landscape - including Astro’s Server Islands feature, which extends the pattern to defer per-component server work.


The mental model

Picture a typical content page: a header, a long article, an image carousel, a comments widget, and a footer. In a traditional SPA, the entire page is one React (or Vue, or Svelte) tree that gets hydrated as a single unit. Touch anything, you pay for everything.

In islands, the picture inverts. The page is server-rendered HTML by default. The carousel and the comments widget are flagged as interactive, so the framework leaves placeholders for them in the HTML and ships only the JavaScript needed to hydrate those two regions. Everything else - the header, the article body, the footer - has zero client-side JavaScript associated with it and never needs to be hydrated.

SSR, progressive hydration and islands architecture

A few properties fall out of this design:

  • Each island is its own root. Two islands on the same page do not share a React tree, a virtual DOM, or a state store unless you explicitly wire one up. A bug in one island cannot crash another.
  • Hydration is parallel and independent. Islands hydrate when their schedule says so - on load, on idle, on visibility, on first interaction - not in a single top-down pass.
  • The HTML is the source of truth for static content. No __NEXT_DATA__-style hydration payload is needed for the parts that never become interactive.

This is what separates islands from “progressive hydration” in a traditional SSR framework. Progressive hydration still treats the page as one tree and decides the order in which to hydrate its branches. Islands never assemble that tree on the client at all.


What hydration directives actually look like

The clearest way to see the pattern is through Astro’s hydration directives. Each client:* directive on a component is a contract about when it should become interactive:

---
import LikeButton from "../components/LikeButton.tsx";
import Comments from "../components/Comments.tsx";
import VideoPlayer from "../components/VideoPlayer.tsx";
import RelatedArticles from "../components/RelatedArticles.tsx";
---

<article>
  <h1>Why your bundle is too big</h1>
  <p>Most pages ship JavaScript they do not need...</p>

  {/* Hydrates immediately - the user might click it before scrolling */}
  <LikeButton client:load count={142} />

  {/* Hydrates only when it scrolls into view - it lives at the bottom */}
  <Comments client:visible postId="123" />

  {/* Hydrates when the main thread is idle */}
  <VideoPlayer client:idle src="/intro.mp4" />

  {/* No directive at all - server-rendered HTML, zero JS shipped */}
  <RelatedArticles posts={posts} />
</article>

RelatedArticles is interesting precisely because it has no directive. It is server-rendered, ships as static HTML, and contributes zero bytes of JavaScript to the page. In a traditional SPA every one of these components would have to be in the bundle whether or not the user ever interacts with it.

Fresh (Deno’s island framework) takes a stricter approach: any component placed in routes/ is server-only, and only components placed in islands/ become interactive. The directive is implicit in the directory.


Server Islands

Astro shipped Server Islands in late 2024, and it is worth understanding because it removes one of the long-standing constraints of the original pattern. Classic islands assume the page can be fully server-rendered up-front; if any region depends on slow or per-user data (a “Hello, Alice” greeting, a cart count, recommended products), the entire page either has to wait or be pushed to the client.

Server Islands let you mark a component as deferred, with a fallback that ships in the initial HTML. The component renders on the server when its slot fetches in, then swaps in:

---
import Avatar from "../components/Avatar.astro";
import GenericAvatar from "../components/GenericAvatar.astro";
import Recommendations from "../components/Recommendations.astro";
---

<header>
  <Avatar server:defer>
    <GenericAvatar slot="fallback" />
  </Avatar>
</header>

<main>
  <article>...mostly static content...</article>
  <Recommendations server:defer userId={Astro.cookies.get("uid")}>
    <p slot="fallback">Loading recommendations...</p>
  </Recommendations>
</main>

The page itself becomes cacheable at the edge (no per-user data in the cached HTML), while the per-user fragments render on demand. It is the same separation that islands brought to client hydration, applied to server rendering: a coarse-grained “this page” boundary becomes many fine-grained “this island” boundaries.


Islands vs RSC vs Qwik

The “ship less JavaScript” goal is not unique to islands. Two adjacent paradigms aim at the same target with different mechanics:

React Server Components keep the unified React tree but mark individual components as server-only. A server component renders to a special serialized format (not HTML, not JSON) that React streams to the client and reconciles into the existing tree. Server components ship zero client JS and can read directly from the database, but the model still assumes one big React tree and one React runtime on the client to receive the stream.

Qwik’s resumability rejects hydration entirely. Instead of re-running components on the client to attach listeners, Qwik serializes listener references into the HTML (e.g., on:click="./chunk.js#handler") and uses one tiny global listener to look them up and load chunks on demand. The page is “resumable” from the server’s state without ever re-executing component code. The result is similar to islands - very little JavaScript runs upfront - but the architecture is different: there are no explicit island boundaries because every event is its own island.

The trade-offs:

ApproachWhat gets shippedWhen does code runBest fit
IslandsJS only for islands you mark interactivePer-island, on the directive (load, idle, visible)Content-heavy sites: docs, blogs, marketing, commerce
RSCJS only for client components in the treeClient components hydrate as part of one treeApp-style React projects that want server data access
QwikA loader stub; handler chunks on first eventOn interaction, code is downloaded for that one handlerApps where time-to-first-byte and time-to-interactive matter equally
Classic SSRThe whole component treeTop-down hydration on loadHighly interactive single-page apps

Islands and RSC are closer to each other than the discourse suggests - they are both asking “which components actually need client JS?” - but RSC keeps a unified tree and islands does not.


The framework landscape

The islands idea has spread well beyond its origins. The notable implementations:

  • Astro is the reference implementation. It ships components from React, Preact, Svelte, Vue, Solid, and Lit on the same page, supports the full client:* directive set, and added Server Islands in late 2024. Astro 5 brought a new content layer and stable Server Islands.
  • Fresh is Deno’s island framework. JIT-compiled, zero build step, Preact-based. The “islands live in islands/” convention is enforced by the file system.
  • Marko (eBay’s framework, now at version 6) combines streaming SSR with automatic partial hydration - it figures out which components are interactive without you having to mark them.
  • Qwik is the resumability sibling, technically a different paradigm but in the same conversation.
  • îles is Vue-flavored islands, inspired by Astro.
  • Enhance takes an HTML-first take on islands using web components.
  • Slinkity is no longer actively maintained; if you arrived from the older Eleventy + Preact recipe, Astro or Enhance are the modern replacements.

Where islands shine, and where they don’t

The pattern is a near-perfect fit when the page is mostly static and the interactive parts are locally scoped:

  • Documentation, blogs, news, marketing pages
  • E-commerce product pages, where only the gallery and the add-to-cart button need JS
  • Dashboards where most cards are read-only

It is a poor fit when the interactive parts are globally scoped - when shared state has to flow between many widgets, when a route change needs to update half the page, or when the page is fundamentally one big interactive surface (a collaborative editor, a Figma-like canvas, a video conferencing UI). For those, a unified component tree - SPA, SSR with hydration, or RSC - is usually a better fit.

The other practical consideration: inter-island communication. Two islands cannot share React state directly. You can move shared state to URL params, cookies, a tiny store mounted as its own island, or custom DOM events. Astro and Fresh both document patterns for this, but it is a real friction point if you find yourself reaching for it often. When you do, it is usually a signal that you have crossed out of “mostly static” territory.

Image Courtesy: https://divriots.com/blog/our-experience-with-astro/


Trade-offs

The wins are concrete: a typical Astro docs site ships 80%+ less JavaScript than an equivalent Next.js or Nuxt site. The HTML-first output is excellent for SEO and accessibility, and the per-island isolation makes it hard for one bad component to drag down the whole page.

The costs are real too. You give up the simplicity of “one component tree, one mental model”. You have to think explicitly about which components need to be interactive and how (if at all) they communicate. The ecosystem is younger than React’s or Vue’s, and large open-source component libraries are still mostly written for SPA frameworks. Migrating an existing SPA to islands is non-trivial - it is usually easier to start fresh.

For a content-heavy site in 2025, islands - and especially Astro with Server Islands - is probably the default worth challenging. For a heavily interactive web app, it is a tool in the kit, not the foundation.


Further reading