Quick Answer

Islands architecture renders a page as plain static HTML by default and only ships JavaScript for the specific components that actually need to be interactive, each hydrated on its own, independently of the rest of the page. This site runs on Astro, which is the framework that popularised the pattern, so a comment widget or a search box becomes a small island sitting in an ocean of HTML the browser never has to run through a JavaScript framework at all.

The Problem: Most of a Page Doesn't Need to Be an App

Take a typical content page — a blog post, a course lesson, a product page. Maybe 5% of it is genuinely interactive: a search box, a "like" button, an accordion. The rest is text, images, and markup that never changes once it's rendered.

A traditional single-page-app framework doesn't see that distinction. It renders the whole page as one JavaScript component tree, and to make any part of it interactive, the standard hydration model requires the browser to download the JS for the entire tree, parse it, execute it, and re-walk the whole DOM attaching event listeners and reconstructing component state — even for the 95% of the page that was already perfectly fine as static HTML. The browser effectively re-does work it had already finished the moment the HTML arrived.

This is why heavy client-rendered frameworks can show a fully painted page and still have a slow Time to Interactive — the pixels are there, but the JavaScript hasn't finished hydrating yet, so clicks and taps queue up uselessly. For content-heavy sites, that cost is almost entirely wasted: nobody needed the footer, the article body, or the related-posts list to be "hydrated" in the framework sense, because none of it was ever going to change in response to anything the user does.

Islands architecture starts from the opposite assumption: static HTML is the default, and a component only gets JavaScript if you explicitly say so. The amount of JS a page ships scales with how many interactive widgets are actually on that page, not with the total size of the app's codebase.

What an 'Island' Actually Is

An island is a small, self-contained interactive component surrounded by static HTML that was never handed to a JavaScript framework at all — sometimes described as "islands in a sea of static HTML." The rest of the page isn't a degraded or simplified version of a framework app; it's just HTML, the same way a hand-written page from 2005 was just HTML, and the browser renders it exactly that cheaply.

Each island is hydrated independently. If a page has three islands — a newsletter signup form, a comments widget, and an image carousel — each one ships only its own JavaScript, and each one hydrates on its own schedule, with no requirement that they load together or share a single app-wide hydration pass. One island failing to load, or loading late, has no effect on the other two or on the static content around them.

This is a genuinely different relationship between markup and JavaScript than "SSR plus hydrate everything," which is still the default in many frameworks: there, the server sends HTML as a performance optimisation for first paint, but the client is still expected to re-render and attach behaviour to the entire tree before the page is considered fully interactive. Islands skip that step for everything that isn't explicitly marked as an island in the first place.

The boundary of an island is literally the component you've opted in — nothing automatic happens beyond it. In Astro specifically, this distinction maps onto a real file-type difference: .astro components are a build-time templating format that compiles down to HTML and never ships as JavaScript to the browser at all, under any circumstance. Only framework components — a React, Vue, Svelte, or Solid component you've imported into an .astro file — are even eligible to become an island, because those are the only component types that have a client-side runtime to hydrate in the first place. A page built entirely from .astro components, with no framework components at all, ships zero framework JavaScript by construction, not by configuration.

How Astro Implements Islands (With Real Directives)

Astro ships zero JavaScript for a component by default. You opt a component into becoming an island with a client:* directive on the tag where you use it:

---
import Counter from '../components/Counter.jsx';
---
<Counter client:visible />

The directive controls when hydration happens, and each one targets a different kind of component:

  • client:load — hydrates immediately after the page finishes loading. Use this for critical, above-the-fold interactive elements the user is likely to touch right away.
  • client:idle — defers hydration until the browser's main thread is idle (via requestIdleCallback), so a lower-priority widget doesn't compete with more important work during initial load.
  • client:visible — hydrates only once the component scrolls into the viewport, using an IntersectionObserver under the hood. Ideal for anything below the fold — a comments section, a related-content carousel — since the browser never does the hydration work until the user is actually about to see it.
  • client:media — hydrates only when a given CSS media query matches, for components that are only relevant on, say, mobile widths (a slide-out nav drawer that doesn't exist as an interactive element on desktop).
  • client:only — skips server-side rendering entirely and renders purely on the client. Needed for components that depend on browser-only APIs and can't produce meaningful output during SSR.

Every one of these except client:only still server-renders real HTML for the component first — the island arrives on the page already looking correct, and the directive only controls when (or whether) it becomes interactive, not whether it's visible at all.

You can also stack logic: client:visible={{ rootMargin: "200px" }} accepts an options object that passes straight through to the underlying IntersectionObserver, letting you start hydration slightly before the component physically enters the viewport — useful for anything with its own loading flicker you'd rather hide behind a small scroll buffer. And because each directive is just an attribute on a single component tag, nothing stops you from mixing priorities across one page: a chat widget on client:load, a related-articles carousel on client:visible, and a mobile-only nav drawer on client:media="(max-width: 50em)", each hydrating on its own completely independent schedule with no coordination required between them.

Partial Hydration vs Full-Page Hydration

In a conventional client-rendered SPA, "hydration" is an all-or-nothing event: the framework attaches itself to the entire rendered tree in one pass, because the whole page is one component tree as far as the framework is concerned. There's no notion of "this part doesn't need to be hydrated" baked into the model — you'd have to bolt that on yourself with lazy-loading boundaries.

Islands make that boundary the default unit of work. Instead of one hydration pass covering the whole page, you get as many independent hydration passes as you have islands, each with its own directive controlling priority and timing. A page with one client:load island and four client:visible islands below the fold ships and runs the client:load component's JS immediately, and simply never runs the other four unless the user scrolls that far — not "runs them later," but doesn't execute that JavaScript at all until it's needed.

It's worth noting that other frameworks have converged on reducing client JS from different directions — React Server Components, for instance, draw a server/client boundary inside a single component tree rather than treating the whole page as static by default. Different mechanism, overlapping goal: less JavaScript reaching the browser than "hydrate everything" would require.

There's a subtler benefit to the per-island model that's easy to miss: failure isolation. In a full-hydration SPA, a single component throwing during hydration can, depending on your error boundary setup, take down interactivity for the whole tree, since the whole page is one hydration pass. With islands, a bug in one component's client-side code is scoped to that one island — the static content around it was never handed to a JavaScript runtime in the first place, so it has nothing to fail. A broken comments widget stays broken in its own corner of the page; the article above it, the navigation, and the footer keep working exactly as they would have if the widget didn't exist at all.

Where the Islands Model Gets Uncomfortable

Islands are isolated by design, and that isolation has a real cost once you need two of them to talk to each other. There's no built-in shared state between islands the way there is between components inside one framework tree — if a "filter" island needs to update a "results list" island elsewhere on the page, you need an explicit mechanism: a small shared store (Astro's own tooling favours nanostores for exactly this), a custom DOM event one island dispatches and the other listens for, or state pushed through the URL. It works, but it's a deliberate extra step that a single-tree SPA gets for free.

Mixing frameworks per island is a genuine Astro superpower — a React island next to a Vue island next to a Svelte island on the same page is a real, supported thing — but each framework you mix in ships its own runtime. Do that casually across many components and you can end up shipping more total JavaScript than if you'd picked one framework and stuck to it, even though each individual island is small.

And the model's biggest mismatch is architectural, not technical: islands architecture is built for pages that are mostly static with sprinkles of interactivity — marketing sites, blogs, documentation, course content. It's a poor fit for an application that is the interactivity end to end, like a spreadsheet, a code editor, or a real-time dashboard, where there's no meaningful "static ocean" for the islands to sit in — nearly the entire page is interactive, so you gain little from a pattern whose whole value is skipping hydration for the parts that aren't.

Islands Beyond Astro

Astro didn't invent the idea — the term predates the framework — but it's the implementation most developers encounter it through today. A few other takes on the same core idea, worth knowing exist even without going deep on each:

  • Deno Fresh uses islands by file-system convention: components placed in an islands/ directory are automatically treated as hydration boundaries, with everything else rendered as static server output.
  • Marko (eBay's framework) does fine-grained partial hydration at a more granular level than whole-component boundaries, compiling down to very targeted DOM updates.
  • Qwik takes a genuinely different approach to the same underlying complaint about hydration cost — rather than scoping hydration down to small islands, it uses "resumability," serializing a component's execution state into the HTML itself so the browser can resume exactly where the server left off without re-running setup code at all. It's a different answer to the same question islands ask, not an islands implementation itself.

The common thread across all of them, Astro included, is the same inversion: ship HTML by default, and make JavaScript something you opt into per component rather than something the whole page starts with. Where they genuinely differ is in how deliberate that opt-in is — Fresh infers it from a file's location in your project, Astro from an explicit directive on the tag itself, and Marko from compiler-level analysis of which parts of a template can actually change at runtime. None of these approaches requires you to commit your whole project to a single framework's hydration model the way a conventional SPA does; the decision about how much JavaScript a given piece of the page needs gets made at the component level, not the application level.

Is Your Project a Good Fit for Islands?

None of this makes islands architecture a universal upgrade over a conventional SPA — it's a trade-off suited to a specific shape of project, and it's worth being honest about which shape yours is before adopting it. The checklist below is less about "which is better" and more about which column your actual project lands in most often.

SignalFavours islandsFavours a full SPA
Ratio of static content to interactive widgetsMostly static (blog, docs, marketing, course pages)Mostly interactive (dashboard, editor, chat app)
Need for shared client-side state across the pageMinimal — a few isolated widgetsHeavy — state flows across most of the UI
SEO / first-paint priorityHigh — static HTML is the whole pointUsually handled separately via SSR/streaming
Team's framework preferenceCan mix frameworks per island if genuinely neededSingle framework, single mental model throughout

If most of your checklist lands in the first column, islands will likely cut real, measurable JavaScript off your page weight. If it lands in the second, you'll probably spend more effort working around the isolation than you'd save on hydration cost.

It's also worth remembering this isn't necessarily a project-wide, one-time decision. A reasonably common real setup is a mostly-static marketing or content site built with islands for its public-facing pages, with a genuinely app-like dashboard or editor living at a specific route of that same site, built with a conventional full-hydration approach for that one section specifically. Nothing about adopting islands architecture for most of a site obligates you to force every page into that same mould — the checklist above is a per-page or per-route question just as much as it's a whole-project one, and treating it that way usually gets better results than picking a single architecture and bending every page to fit it.

Frequently Asked Questions

Does islands architecture mean a page ships zero JavaScript?

Only for the parts you don't explicitly mark as islands. A page with no interactive components at all genuinely ships no framework JavaScript, but the moment you add a client:load or client:visible component, that component's JS ships like normal — islands just mean the rest of the page doesn't have to.

Can two islands on the same page communicate with each other?

Not automatically. Islands hydrate independently and don't share state by default, so talking between them needs an explicit mechanism — a small shared store, a custom DOM event, or state passed through the URL.

Is islands architecture specific to Astro?

No — Astro is the most widely used implementation, but Deno Fresh and Marko both implement variations on the same pattern, and the underlying idea predates all of them.

Does server-side rendering still happen for an island?

Yes, for every directive except client:only. The server renders real HTML for the component first, and the directive only decides when (or whether) that HTML becomes interactive on the client.

Is this the same thing as React Server Components?

They solve an overlapping problem — less client-side JavaScript than 'hydrate everything' — through a different mechanism. React Server Components draw a server/client boundary inside a single React component tree; islands architecture is framework-agnostic and treats the whole page as static HTML by default, with interactivity as an explicit, isolated opt-in per component.