Islands Architecture
Ship zero JavaScript by default. Hydrate only what needs interactivity.
The Concept
Most of any web page is static content: headers, text, images, footers. Only small pieces actually need JavaScript for interactivity -- a counter, a search bar, an interactive chart. Islands architecture treats each interactive piece as an independent "island" that hydrates on its own, while the rest of the page stays as pure static HTML.
The result: your pages load faster because the browser downloads and executes far less JavaScript. A blog post with a single comment widget only ships JS for that widget, not for the entire page.
Hydration Modes
What gives you fine-grained control over when each island hydrates. Choose the mode that matches the island's purpose:
client:load
Hydrate immediately when the page loads. Use this for islands that must be interactive right away, such as a navigation menu or authentication widget.
Island({ name: 'NavMenu', mode: 'load' })
client:idle
Hydrate when the browser is idle, using requestIdleCallback. This is the default mode. Good for interactive elements that are not needed in the first moments of the page, such as a "like" button or share widget.
Island({ name: 'LikeButton', mode: 'idle' })
client:visible
Hydrate when the island scrolls into the viewport, using IntersectionObserver with a 200px root margin. Ideal for content below the fold: comment sections, related posts, charts that the user might never scroll to.
Island({ name: 'CommentSection', mode: 'visible' })
client:interaction
Hydrate on the first user interaction -- click, focus, hover, or touch. The island renders as static HTML until the user actually tries to use it. Perfect for accordions, tabs, or dropdown menus.
Island({ name: 'Accordion', mode: 'action' })
client:media
Hydrate when a CSS media query matches. Use this for mobile-only or desktop-only interactivity. For example, a hamburger menu that only needs JS on small screens:
island('MobileMenu', () => import('./MobileMenu.js'), {
mode: 'media',
media: '(max-width: 768px)',
})
The Island Component
During server rendering, use the Island component to mark where an island should appear. It renders a wrapper <div> with data-island attributes that the client uses for hydration:
import { Island } from 'what-framework/server';
function ProductPage({ product }) {
return h('div', null,
// Static HTML — no JS shipped
h('h1', null, product.name),
h('p', null, product.description),
h('img', { src: product.image, alt: product.name }),
// Interactive island — JS loaded on interaction
Island({
name: 'AddToCart',
mode: 'action',
props: { productId: product.id, price: product.price },
}),
// Island that hydrates when scrolled into view
Island({
name: 'ReviewSection',
mode: 'visible',
props: { productId: product.id },
}),
);
}
Props passed to the Island component are serialized as JSON in a data-island-props attribute and automatically deserialized when the island hydrates on the client.
Registering Islands
On the client side, register each island with a name and a dynamic import loader. This tells What which component to load when it is time to hydrate:
import { island, hydrateIslands } from 'what-framework/server';
// Register islands with lazy loaders
island('AddToCart', () => import('./components/AddToCart.js'));
island('ReviewSection', () => import('./components/ReviewSection.js'));
island('NavMenu', () => import('./components/NavMenu.js'), {
mode: 'load',
priority: 10,
});
// Find all [data-island] elements and schedule hydration
hydrateIslands();
You can also use autoIslands to register and hydrate in one step:
import { autoIslands } from 'what-framework/server';
autoIslands({
AddToCart: { loader: () => import('./components/AddToCart.js'), mode: 'action' },
ReviewSection: { loader: () => import('./components/ReviewSection.js'), mode: 'visible' },
NavMenu: { loader: () => import('./components/NavMenu.js'), mode: 'load' },
});
Island Stores
When multiple islands need to share state (for example, a cart icon in the header and an "Add to Cart" button in the body), use createIslandStore. These stores are serialized during SSR and hydrated on the client so state survives the server-to-client transition:
import { createIslandStore } from 'what-framework/server';
// Create a shared store — same name = same instance
const cartStore = createIslandStore('cart', {
items: [],
count: 0,
});
// Read and write like a normal object
cartStore.count; // reactive read
cartStore.count = 5; // reactive write
cartStore.items = [...cartStore.items, newItem];
On the server, call serializeIslandStores() to embed state into the HTML. On the client, hydrateIslandStores() restores it before islands hydrate:
// Server-side: embed store state in the HTML
const storeData = serializeIslandStores();
const html = `
<script data-island-stores>${storeData}</script>
`;
// Client-side: hydrateIslands() automatically restores stores
// from any <script data-island-stores> tag on the page
Progressive Enhancement
Not every interactive feature needs a full island. For simple enhancements -- like a form that submits via fetch instead of a page reload -- use the progressive enhancement helpers:
import { enhance, enhanceForms } from 'what-framework/server';
// Enhance all forms with data-enhance attribute
enhanceForms();
// Or target specific elements with a custom handler
enhance('.copy-button', (el) => {
el.addEventListener('click', () => {
navigator.clipboard.writeText(el.dataset.text);
el.textContent = 'Copied!';
});
});
Enhanced forms dispatch form:response and form:error custom events so you can react to the result without mounting a full component:
// In your HTML
// <form data-enhance method="POST" action="/subscribe">
// <input name="email" type="email" />
// <button>Subscribe</button>
// </form>
// Listen for the response
document.querySelector('form').addEventListener('form:response', (e) => {
if (e.detail.ok) {
e.target.innerHTML = '<p>Subscribed!</p>';
}
});
Priority Hydration
When multiple islands are scheduled to hydrate at the same time, What processes them in priority order. Higher priority islands hydrate first:
// Critical island — hydrates first
island('SearchBar', () => import('./SearchBar.js'), {
mode: 'load',
priority: 10,
});
// Low priority — hydrates after everything else
island('Analytics', () => import('./Analytics.js'), {
mode: 'idle',
priority: 0,
});
You can also boost an island's priority at runtime with boostIslandPriority(name, newPriority), for example when a user starts interacting near a pending island.
Debugging
Call getIslandStatus() at any time to inspect the current state of the island system:
import { getIslandStatus } from 'what-framework/server';
const status = getIslandStatus();
console.log(status);
// {
// registered: ['AddToCart', 'ReviewSection', 'NavMenu'],
// hydrated: 1,
// pending: 2,
// queue: [
// { name: 'AddToCart', priority: 0 },
// { name: 'ReviewSection', priority: 0 },
// ],
// stores: ['cart'],
// }
Each island also dispatches an island:hydrated custom event when it finishes hydrating, which you can use for performance tracking or analytics.
Islands vs Regular Components
Islands are designed for multi-page applications with server-side rendering. Each page is its own HTML document, and islands add interactivity to specific parts. If you are building a single-page application where the whole page is already client-rendered, use regular components instead -- there is no benefit to the islands pattern when all your JavaScript is already loaded.