I did a stupid thing last year. I built my simple personal blog using Next.js.
Don't get me wrong, Next.js is incredible. But shipping 200KB of JavaScript just to render a static page of text is like driving a Ferrari to pick up groceries. It’s cool, but the gas mileage is terrible, and you look kind of ridiculous stuck in traffic.
So last weekend, I stared at my Lighthouse score (which was an embarrassing "Orange") and decided to rewrite everything in Astro.
The result? My jaw hit the floor. 100/100 performance score, almost by accident.
Why Astro Felt Like cheating
The magic of Astro isn't what it adds; it’s what it subtracts. By default, Astro ships Zero JavaScript. None. Nada.
If you write a card component in React:
// Card.jsx
export function Card({ title }) {
return (
<div onClick={() => console.log("clicked")}>
<h1>{title}</h1>
</div>
);
}
When you wrap this in Astro, it runs that component on the server, strips out the React library, strips out the event listeners, and just serves the raw HTML <div>.
"But wait," I hear you ask. "What if I need the click listener? What if I need interactivity?"
The "Island" Aha Moment (Partial Hydration)
This is where Astro changed my mental model. They call it Island Architecture. Think of your page as a sea of static HTML (fast, light, cheap). Inside that sea, you can drop small "islands" of interactivity that "hydrate" into full React/Vue/Svelte apps.
---
// This runs on the server (build time)
import Header from './Header.astro';
import InteractiveCounter from './Counter.jsx';
---
<!-- Static Header (0 JS sent to client) -->
<Header />
<!-- Interactive React Component (Hydrated!) -->
<InteractiveCounter client:load />
<!-- Lazy Loaded Component (Hydrated only when visible) -->
<InteractiveCounter client:visible />
That client:visible directive is the game changer. It means the JavaScript for that component isn't even downloaded until the user scrolls it into view.
It’s not just an optimization; it’s a philosophical shift. You opt-in to complexity only when you need it.
Data Fetching: Goodbye useEffect
One of the biggest pain points in React is fetching data. You have to deal with useEffect, loading states, empty states, and race conditions.
In Astro, your component script runs on the server. You can write top-level awaits like you're writing a Node.js script.
React (The Old Way):
function Post() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/post")
.then((res) => res.json())
.then(setData);
}, []); // Don't forget the dependency array!
if (!data) return <Spinner />;
return <h1>{data.title}</h1>;
}
Astro (The Better Way):
---
const res = await fetch('https://api.mycms.com/post');
const data = await res.json();
---
<h1>{data.title}</h1>
No hooks. No client-side waterfalls. Just HTML arriving fully formed from the server.
The Trade-offs (What I Miss About Next.js)
To be fair, Astro isn't a silver bullet. If you are building an application with complex state-sharing—like a Dashboard or a Social Network where clicking a "Like" button needs to update the header notification count instantly—Astro can be tricky. Because it's a Multi-Page Application (MPA) by default, navigating between pages triggers a full browser reload. State stored in React Context or Redux gets wiped out.
You can solve this with their new "View Transitions" API and persistent islands, but Next.js handles complex client-side routing more naturally out of the box.
Should You Switch?
- Use Next.js/Remix if: You are building a complex web app (Gmail, Twitter/X, SaaS Dashboard).
- Use Astro if: You are building a content-heavy site (Blog, Portfolio, Marketing Page, Documentation).
For my blog, switching to Astro made the site lighter, faster, and ironically, easier to maintain because I deleted more code than I wrote. And I didn't have to sacrifice my React knowledge to get there.