After years of building sites in WordPress and then moving to React, one thing became obvious to me: most of the sites I build do not need that much JavaScript. A company site, a blog, a portfolio, they are mostly static content. But with Next.js or Nuxt the visitor still downloads hundreds of KB of JavaScript just to read an article.
Astro solves exactly that problem. By default it ships no JavaScript at all to the browser, and only hydrates the parts you explicitly mark as interactive. That is the “islands” model.
What the islands architecture is
Picture a product detail page. Most of the screen is images, description and specs, things that never change after the page loads. Only a few spots really need JavaScript: the quantity picker, the add to cart button, a swipeable image gallery.
In a traditional React app the whole page is one component tree hydrated from top to bottom. The browser has to download React, download your code, rebuild the entire tree, and only then can it attach event handlers.
With Astro each interactive piece is its own island sitting in a sea of static HTML:
---
import ProductGallery from "../components/ProductGallery.jsx";
---
<h1>{product.name}</h1>
<p>{product.description}</p>
<!-- Only this component gets hydrated -->
<ProductGallery client:visible images={product.images} />
The client:visible directive tells Astro to load the JavaScript for this component only when the user scrolls to it. If a visitor just reads the description and leaves, they download zero bytes of JavaScript.
Astro has a few other directives, and picking the right one matters more than most people think:
client:loadhydrates as soon as the page loads. Use it for things that must be interactive immediately, like a mobile menu.client:idlewaits until the browser is idle. Good for things that matter but are not urgent.client:visiblewaits until the component enters the viewport. This is the one I reach for most.client:mediaonly hydrates when a media query matches. Great for components that only exist on mobile.client:onlyskips server rendering entirely. You need this when a component depends onwindoworlocalStorage.
Content Collections
This is the feature that made me stay with Astro. Instead of reading Markdown files and guessing what fields the frontmatter has, you declare a schema with Zod:
import { defineCollection, reference, z } from "astro:content";
import { glob } from "astro/loaders";
const post = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/post" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
category: reference("category"),
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false),
}),
});
The real value is this: if I misspell a category name in a post, or forget the description field, the build fails with a message pointing at the exact file and line. The error gets caught at build time instead of when a client finds a blank page.
The reference("category") helper also creates a real link between two collections. Astro checks that the referenced category exists, and in your templates you fetch it with getEntry() with full type hints.
z.coerce.date() is worth a mention too: it turns the 2026-05-28 string in the frontmatter into a real Date object, so you can sort and format dates without parsing anything yourself.
Framework agnostic
Astro does not make you pick a side. The same project can use React here, Svelte there, and mostly plain .astro components that cost no JavaScript at all.
In practice I ended up using far less framework code than I expected. Once static HTML is the default, a lot of things that seemed to need React turn out to need a few lines of plain JavaScript. This site uses no UI framework at all, just Astro with Bootstrap and Sass.
What I learned building this site
.astro components should be the default. Only reach for a React or Svelte component when you genuinely need client-side state. Every island you add is another JavaScript bundle.
Watch the server and client boundary. The frontmatter between the two --- markers runs at build time, on the server. It can read files, call APIs, query a database. But props passed into an island have to be serialisable, so do not try to send functions or class instances through it.
Check bundle size early. It is easy to accidentally pull a heavy library into an island. Run astro build and look inside the dist folder to see what you are actually shipping.
View Transitions give you the SPA feel. Astro supports smooth page transitions out of the box without turning your site into a single page app. You keep the static HTML and still get the animation.
When not to use Astro
To be fair, Astro is not right for everything. If you are building an app where nearly every screen is stateful, say an admin dashboard, an editing tool, or a booking system with a constantly interactive calendar, the islands model starts to feel forced. That is where Next.js, Remix or a plain React SPA is the more natural pick.
The line I use to decide: if I notice that more than half my components are becoming islands, that is a sign I picked the wrong tool.
The actual result
This site builds 26 static pages in about 10 seconds and ships basically nothing but HTML and CSS. It runs on the Cloudflare Pages free tier and there is not much left in it that could be slow.
For a personal brand site, Astro hits the right balance between developer experience and page speed.