A practical islands architecture playbook to ship less JavaScript, cut hydration costs, and make pages feel instant — without killing interactivity.

Learn 10 islands architecture plays to reduce JS, delay hydration, stream dynamic "holes," and boost Core Web Vitals without losing UX.

Let's be real: most "modern" web pages don't feel modern. They feel… busy. Like the browser is doing paperwork before it's allowed to show the content.

Islands architecture is one of the cleanest ways out: render most of the page as HTML, then hydrate only the parts that truly need to be interactive. Astro's docs describe the core idea as server-rendered HTML with "dynamic regions" that become small hydrated widgets on the client (also called partial/selective hydration).

This article is a field guide. Not theory. Ten plays you can run this week.

The islands mental model (in one sketch)

Think of your page like a map with a few "live zones":

Server renders HTML (fast, cacheable)
┌─────────────────────────────────────────┐
│ Header (static)                         │
│ Article content (static)                │
│                                         │
│   [ Island: Search Filters ]  <-- JS    │
│                                         │
│ Testimonials (static)                   │
│   [ Island: Pricing Toggle ] <-- JS     │
│ Footer (static)                         │
└─────────────────────────────────────────┘
Client loads tiny bundles only for islands

A nice side effect: you stop treating the whole page like an app shell. You treat it like… a web page. And pages are allowed to be fast.

1) Start "HTML-first" and earn every byte of JS

Play: Default to SSR/SSG markup; make client JS opt-in.

In Fresh, for example, routes render on the server and ship zero JavaScript by default, then you add interactivity via islands. Whether you use Fresh, Astro, or your own setup, the tactic is the same:

  • If content is readable without JS, keep it that way.
  • Only "promote" UI to an island when it truly needs state, events, or real-time updates.

Real-world check: marketing pages, docs, blogs, SEO landing pages — these are mostly static. Treat them like it.

2) Do an "interaction inventory" before you code anything

Play: List every interactive element and classify it.

A classic set of e-commerce examples: Add to Cart, image carousel, cart button — those are the islands. That's your inventory template:

  • Must be interactive immediately: cart count, primary CTA
  • Can wait until visible: carousels, accordions, maps
  • Can wait until intent: "open filters," "compare plans," "start chat"
  • Shouldn't be JS at all: tabbed content that can be anchors, hover menus that can be CSS

This one exercise stops "accidental SPA."

3) Use partial hydration, not "hydrate the universe"

Play: Hydrate only the island components — never the whole page.

This is the essence of islands architecture + partial/selective hydration.

If your framework supports it, lean in:

  • Astro islands
  • Fresh islands
  • Gatsby partial hydration (built around React Server Components)

If you're rolling your own, the same rule applies: mount small widgets into server-rendered slots.

4) Delay hydration by visibility (your users can't click what they can't see)

Play: Hydrate "below the fold" islands only when they enter the viewport.

This is the biggest "free lunch" in islands land. Why ship and run carousel JS before the user scrolls?

A framework might give you a directive (e.g., "hydrate when visible"). If not, you can DIY with IntersectionObserver:

// hydrate-on-visible.js
const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (e.isIntersecting) {
      const el = e.target;
      io.unobserve(el);
      // Load the island bundle only now
      import(el.dataset.island).then((m) => m.mount(el));
    }
  }
});

document.querySelectorAll("[data-island]").forEach((el) => io.observe(el));

Commentary: This pattern is boring… and that's why it works.

5) Delay hydration by intent (click, hover, keypress)

Play: Hydrate only after a meaningful user action.

Think filters panel. Chat widget. Compare drawer. You can render a server-side placeholder and only load code when someone clicks:

<button id="openFilters">Filters</button>
<div id="filters" data-island="./filters-island.js"></div>
<script type="module">
  document.getElementById("openFilters").addEventListener("click", async () => {
    const el = document.getElementById("filters");
    const mod = await import(el.dataset.island);
    mod.mount(el);
  }, { once: true });
</script>

Why it's powerful: you pay the JS cost only for users who actually need it.

6) Delegate events so islands don't boot just to attach listeners

Play: Use event delegation for simple interactions.

A common hydration tax is "attach 200 click handlers." Delegation flips that: one handler at a parent, route by attributes.

document.addEventListener("click", (e) => {
  const btn = e.target.closest("[data-action]");
  if (!btn) return;

  if (btn.dataset.action === "toast") {
    // tiny inline behavior, no island needed
    showToast(btn.dataset.message);
  }
});

Rule of thumb: if behavior doesn't need component state, don't hydrate a component.

7) Stream dynamic "holes" with server-first rendering

Play: Combine static and dynamic in one route — without turning everything into client JS.

A useful mental model: static content can be prerendered/cached while dynamic parts stream in as "holes" in a single request. Even if you're not on a framework that names this feature, the architectural move is worth stealing:

  • Cache the static shell aggressively
  • Stream user-specific sections (auth, recommendations)
  • Keep those dynamic sections server-rendered when possible

This reduces the pressure to ship "just in case" client bundles.

8) Consider resumability when hydration itself is the bottleneck

Play: For highly interactive apps, skip "rehydrate everything" if your framework allows it.

Some frameworks are built around the idea that you don't need hydration to resume an application on the client; avoiding hydration is what enables near-instant startup. You don't need to rewrite your whole stack tomorrow — but it's useful to name the enemy:

  • Hydration is work you do after the page is already rendered
  • If your app is hydration-bound, "partial" helps
  • If you're still hydration-bound, "resumable" is a serious option

9) Make islands boring: strict boundaries + stable contracts

Play: Treat each island like a micro-product with a tiny API.

This is where teams win or lose. Islands can turn into a mess if boundaries are fuzzy.

Practical contracts that scale:

  • Inputs: data-* attributes or serialized JSON in a <script type="application/json">
  • Outputs: DOM events (dispatchEvent(new CustomEvent(...))) or form submissions
  • Styling: shared CSS tokens + scoped styles inside the island

If islands can't talk without importing each other, you're rebuilding a monolith.

10) Enforce a JS budget like you enforce a cloud budget

Play: Put numbers on it, or it won't happen.

A lightweight process that works:

  • Budget: JS shipped, JS executed, and long tasks
  • PR checks: fail builds if a route exceeds thresholds
  • Watch Core Web Vitals regressions, but don't wait for them — catch bloat at PR time

You might be wondering: "Isn't this overkill?" Not anymore. JS bloat is the new slow database query — everyone feels it, and nobody wants to own it unless there's a gate.

A quick "which play do I run first?" checklist

  • Page is mostly content → Plays 1–4
  • UX has heavy optional widgets → Plays 5–6
  • Personalization + cache pressure → Play 7
  • App startup feels sluggish even on fast devices → Play 8
  • Team scaling / lots of contributors → Plays 9–10

Conclusion: islands aren't a framework — they're a discipline

Islands architecture is basically the web's original promise with a modern twist: HTML first, interactivity where it matters. Done well, it's not just faster. It's calmer. Easier to reason about. Easier to cache. Easier to ship.

If you're experimenting with islands right now, drop a comment with your biggest "JS weight" culprit (carousel? analytics? filters?). And if you want a follow-up, I can write a teardown of a real page: where the islands should be, what to defer, and what to delete.

Follow for more performance architecture playbooks.