My old portfolio was a full React + client router setup for a site that is, in practice, five static sections and a contact form. It shipped more JavaScript than a page like that needs. I rebuilt it with Astro and shipped almost none.
What Astro actually does differently
Astro renders components to HTML at build time by default. Nothing hydrates
on the client unless you explicitly opt in with a directive like
client:load or client:visible. For a content-first site, that means the
homepage ships with effectively zero framework JavaScript.
---
// src/pages/index.astro
const projects = await getProjects();
---
<section>
{projects.map((project) => (
<article>
<h3>{project.title}</h3>
<p>{project.description}</p>
</article>
))}
</section>
Content collections replaced my ad-hoc data files
Before, blog-style content lived in a few hardcoded arrays. Astro’s content collections give that structure schema validation and type safety for free:
// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
Querying and rendering a post is just:
---
import { getCollection, render } from "astro:content";
const posts = await getCollection("blog");
const { Content } = await render(posts[0]);
---
<Content />
The tradeoffs
Nothing is free. Here’s what I gave up and what I gained:
| Before (React SPA) | After (Astro) | |
|---|---|---|
| JS shipped on homepage | ~140kb | ~2kb |
| Interactive components | Trivial everywhere | Opt-in, per component |
| Content authoring | Hardcoded arrays | Markdown/MDX with schemas |
| Build output | Client-rendered | Static HTML |
The only place I kept client-side JavaScript is small, targeted enhancements
— like the copy-to-clipboard button on code blocks in this post — added with
a plain <script> tag rather than a framework.
Steps if you’re doing the same migration
- Start with static pages first; don’t reach for
client:*directives until something genuinely needs interactivity. - Move existing content into collections early — retrofitting schemas onto loose data later is more work than doing it up front.
- Keep global styles in one stylesheet so layouts share the same design tokens instead of duplicating CSS variables per page.
The best performance optimization is often just shipping less JavaScript, not shipping the same JavaScript faster.
If you’re evaluating Astro for a similar content-heavy site, the official docs cover content collections and the islands architecture in more depth than I have here.