Why I Rebuilt My Portfolio with Astro
The Problem with SPAs for Content Sites
Most portfolio sites don't need React to render a headline. Yet most developers — myself included — reach for a React SPA by default. The result is hundreds of kilobytes of JavaScript shipped to the browser before a single word of content appears.
I spent two years running my portfolio on Next.js. It was fine. But every Lighthouse audit told the same story: a content site weighed down by a full SPA runtime.
Enter Astro
Astro's core idea is deceptively simple: ship HTML, not JavaScript. It renders your components to static HTML at build time and only sends JavaScript for the parts that need it — what the Astro team calls "islands."
---
// This component produces zero JS in the browser
import HeroText from './HeroText.astro';
---
<HeroText />
<!-- This island hydrates only when visible -->
<AnimatedCounter client:visible />
The client:* directive is the key primitive. You get granular control:
| Directive | When it hydrates |
|---|---|
client:load |
Immediately on page load |
client:idle |
When the browser is idle |
client:visible |
When the component enters the viewport |
client:media |
When a CSS media query matches |
Mixing Frameworks Without the Drama
The part that surprised me most: Astro lets you mix React, Svelte, Vue, and Solid in the same project. Not because you should, but because you can pick the right tool per component.
My portfolio uses React for interactive sections (animations, forms) and zero-JS Astro components for everything static. The build output is leaner, the runtime is minimal, and the DX is excellent.
Real Numbers
After migrating from Next.js to Astro:
- Total JS shipped: dropped from 312 KB to 48 KB
- Time to Interactive: 1.8s → 0.4s on a throttled connection
- Lighthouse Performance: 71 → 97
Astro isn't for every project. If you're building a highly dynamic app with client-side state everywhere, Next.js or Remix are the right tools. But for content-heavy sites — portfolios, blogs, marketing pages — Astro is the most honest choice I've found.
The Part Nobody Talks About
Astro's content collections are underrated. Type-safe, schema-validated MDX files with zero boilerplate. The config is a single content.config.ts file, and you get autocomplete for frontmatter fields in every post.
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
}),
});
If you've been holding off on Astro because it feels like a niche tool — it isn't anymore. Version 5 is production-ready, the ecosystem is mature, and the performance benefits are real.