Astro starts from a premise that, after years of SPAs, sounds almost heretical: by default, your component ships no JavaScript to the browser. None. It renders to HTML at build time and stays there, static and fast. Interactivity isn't the page's natural state, it's something you request explicitly, component by component, with a client: directive. This blog is built that way, and understanding the islands architecture — when to hydrate and, above all, when not to — is the difference between a page that flies and one that drags kilobytes of framework around just to animate a button.
The mental model: HTML by default, JS on demand
In a classic SPA everything is JavaScript: the framework mounts entirely on the client, hydrates the whole tree, and governs the page from there. It's comfortable for the developer and expensive for the user, who pays the cost of booting the framework even when 90% of the screen is text that never changes.
Astro flips the premise. It renders your components to HTML on the server or at build time, and by default discards the JavaScript that generated them. What reaches the browser is plain HTML that paints instantly. The "islands" are the specific zones that do need a life of their own — a carousel, a form with validation, a dropdown menu — surrounded by a sea of static HTML that costs nothing. Each island hydrates independently; the rest of the page never notices.
The client: directives are the decision, not a detail
The lever is the directive you give the component when you use it. Each one describes when that island hydrates, and choosing well is where the performance lives:
---
import Carousel from '../components/Carousel.jsx';
import NewsletterForm from '../components/NewsletterForm.jsx';
import HeavyChart from '../components/HeavyChart.jsx';
---
<!-- Hydrates as soon as the page loads: for what's above the fold -->
<Carousel client:load />
<!-- Hydrates when it enters the viewport: for what's further down -->
<HeavyChart client:visible />
<!-- Hydrates when the browser is idle: for non-urgent stuff -->
<NewsletterForm client:idle />
client:load hydrates immediately: use it only for what's interactive from the first visible pixel. client:idle waits for the main thread to breathe. client:visible is my favorite: it doesn't spend a byte of JavaScript until the component enters the screen, ideal for everything below the fold. The directive isn't a syntax ornament: it's literally the load policy of each island, and placing them carelessly is how JavaScript the user doesn't need sneaks in.
When NOT to hydrate (which is almost always)
The most common mistake for people arriving from React is treating every component as if it needs state. It doesn't. A card, a heading, a list of posts, a footer: all of that is HTML that paints once and never changes again. If there's no event, there's no state and no interaction, there's no island. It stays as a static Astro component and ships zero JavaScript.
The question I ask of every component isn't "can it be interactive?" but "does it have to respond to something from the user on the client?". A button that's just a link isn't an island, it's an <a> tag. An accordion can be built with native <details> and <summary> without a line of JS. The more I push interactivity toward native HTML and CSS, the fewer islands I need and the lighter the page stays.
💡 In Astro, every
client:you write is JavaScript the user downloads. The best optimization isn't hydrating faster, it's not hydrating.
Islands don't share state, and that's on purpose
Here's the trade-off you have to be clear on before marrying the model. Each island is an isolated component: it hydrates on its own and doesn't magically share state with the others. If I'm coming from a SPA where a global store wires everything together, this feels like a limitation. Two islands that need to talk to each other don't do it through props, because they're independent trees mounted separately.
The solution exists — browser events, shared nanostores, signals — but it's real friction, and that friction is the sign that maybe you're using the tool against the grain. If your page is basically an app with tightly interwoven state, many parts talking to each other constantly, Astro will ask you to do gymnastics for something a SPA framework gives you for free. There I don't force Astro: I pick the right tool.
Why I chose this model anyway
Astro shines when content leads and interactivity is occasional: blogs, marketing sites, documentation, portfolios, e-commerce with product islands. That's exactly the shape of most of the web, and of this site. I accept the friction of coordinating islands in exchange for every page being born weightless by default, and for me being the one who decides and justifies every kilobyte of JavaScript I add.
That shift of the default is everything. In a SPA, JavaScript is the norm and removing it takes work; in Astro, JavaScript is the exception and adding it is a conscious decision. I'd rather have a system where the cheap thing is the easy thing and the expensive thing requires me to ask for it on purpose. That inversion of the default burden is, for the kind of sites I build, the best performance decision I can make without optimizing a single line.