Astro: The Content Framework with Islands Architecture

I've been building content-heavy sites with Next.js for years. It's a great framework, but there's always been a nagging issue: even for a blog post that's 100% static text, Next.js ships a JavaScript bundle to the client. React hydration runs on every page load, parsing and executing kilobytes of JS just to make a static page "interactive" (even when there's nothing to interact with).
Astro takes the opposite approach. It ships zero JavaScript by default. Your pages render to pure HTML and CSS on the server. If a component needs interactivity, you explicitly opt it in. Everything else stays static. This is the islands architecture, and after using it for several projects, I think it's the right default for most content sites.
Islands Architecture Explained
Think of your page as an ocean of static HTML. Most of it (headers, text, images, footers) doesn't need JavaScript. Interactive components (a search bar, a comment form, a dark mode toggle) are "islands" of interactivity floating in that static ocean.
In a traditional SPA, the entire page is one big JavaScript application. In Astro's model, only the islands get JavaScript. The rest is pure HTML that loads instantly. The result is dramatically smaller page weights and faster load times.
The key mental shift: instead of opting out of JavaScript (like adding `next/dynamic` with `ssr: false`), you opt in. The default is no JS. You add it where you need it.
Bring Your Own Framework
One of Astro's best features is that islands can use any UI framework. You're not locked into one. A single Astro project can include React components, Svelte components, and Vue components, all on the same page if you want.
---
// src/pages/index.astro
import Header from '../components/Header.astro';
import SearchBar from '../components/SearchBar.tsx';
import Newsletter from '../components/Newsletter.svelte';
---
<Header />
<main>
<h1>Welcome to my blog</h1>
<p>This is static HTML. No JavaScript needed.</p>
<!-- This React component becomes an interactive island -->
<SearchBar client:load />
<!-- This Svelte component hydrates when visible -->
<Newsletter client:visible />
</main>
The `.astro` components render to static HTML with zero client JS. The React and Svelte components only ship their framework code when the `client:` directive tells Astro to hydrate them.
Client Directives
The `client:` directives control when and how islands hydrate. This is where Astro's performance story gets really interesting:
- `client:load` hydrates immediately on page load. Use for critical interactive elements like navigation menus or search bars.
- `client:idle` hydrates after the page finishes loading and the browser is idle. Good for non-critical interactive elements.
- `client:visible` hydrates when the component scrolls into the viewport. Perfect for below-the-fold content like comment sections or newsletter signups.
- `client:media="(max-width: 768px)"` hydrates only when a media query matches. Use for mobile-only interactive elements.
- `client:only="react"` renders only on the client (no SSR). For components that can't render on the server at all.
The combination of `client:visible` and `client:idle` means your above-the-fold content loads with zero JavaScript. Interactive components below the fold don't even download their framework code until the user scrolls to them. This is lazy loading at the framework level.
Content Collections
Astro 2.0 introduced content collections, which provide type-safe frontmatter for Markdown and MDX content. If you're building a blog or docs site, this feature alone is worth the switch.
Define a schema for your content:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
author: z.string(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
image: z.string().optional(),
}),
});
export const collections = { blog };
Now every Markdown file in `src/content/blog/` is validated against that schema at build time. Misspell a frontmatter field? Build error. Forget a required field? Build error. The TypeScript integration means you get autocomplete and type checking when querying your content.
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => {
return !data.draft;
});
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.date.toLocaleDateString()}</time>
<Content />
</article>
The developer experience is excellent. You get full type safety from your content schema through to your templates.
Building a Blog with MDX
Astro has first-class MDX support. Install the integration, and you can use components directly inside your Markdown:
# Install the MDX integration
npx astro add mdx
Then in any `.mdx` file, you can import and use components:
---
title: "Interactive Tutorial"
date: 2023-07-01
---
# Learning Astro
Here's a regular paragraph in Markdown.
import CodePlayground from '../../components/CodePlayground.tsx';
<CodePlayground client:visible code="console.log('hello')" />
And we're back to regular Markdown content below the component.
The code playground component only hydrates when the user scrolls to it. The rest of the tutorial page is static HTML. This combination of rich interactivity with zero-JS defaults is what makes Astro special for content.
Performance: The Numbers
I rebuilt a documentation site (roughly 50 pages) that was previously on Next.js. The before and after Lighthouse scores:
- Next.js: Performance 87, First Contentful Paint 1.2s, Total Blocking Time 340ms, JS bundle 186KB gzipped
- Astro: Performance 99, First Contentful Paint 0.4s, Total Blocking Time 0ms, JS bundle 0KB (no interactive components on docs pages)
Zero Total Blocking Time is the headline. When you ship no JavaScript, there's nothing to block the main thread. Pages feel instant because they are instant. The browser receives HTML and renders it. Done.
Even on pages with interactive islands, the numbers are strong because only the island's code loads, not an entire framework runtime for the page.
When Astro Beats Next.js
Astro is the better choice for: blogs, documentation sites, marketing pages, portfolio sites, news sites, and any content where most pages are primarily read-only. Basically, if the main user action is reading, Astro wins.
When Next.js Still Wins
Next.js is better for: dashboards, SaaS applications, e-commerce with complex cart/checkout flows, real-time collaborative tools, and anything where most of the page is interactive. If the user is clicking, typing, and manipulating state on every page, the SPA model is still the right tool.
The decision is really about the ratio of static content to interactive elements. High content, low interactivity: Astro. High interactivity, content is secondary: Next.js. Many sites fall somewhere in between, and for those, I'd lean Astro and use islands for the interactive parts.
Getting Started
The quickest way to try Astro:
npm create astro@latest
# Choose a starter template
# Select your preferred framework integrations
The CLI walks you through setup and lets you pick integrations (React, Vue, Svelte, Tailwind, MDX) during initialization. The documentation at docs.astro.build is some of the best I've seen for any framework. Within an afternoon, you can have a fully functional blog with content collections, MDX support, and perfect Lighthouse scores.
Astro changed how I think about the default technology choice for content sites. Zero JavaScript as the default, with explicit opt-in for interactivity, is the right mental model. It took us years of framework complexity to come back around to what the web does best: serve HTML fast.