Why I Ditched Next.js and Rebuilt My Site with Astro
Why I Ditched Next.js and Rebuilt My Site with Astro
Sometimes the best code is the code you don’t ship to the browser.
// package.json diff
- "next": "15.3.5",
- "react": "19.0.0",
- "framer-motion": "12.23.0",
- "gsap": "3.13.0",
+ "astro": "6.x",
+ // that's... kind of it
Let me be clear upfront: I still love Next.js. I spent months building my v1 portfolio with it. React 19, Tailwind v4, Framer Motion, GSAP, dual design modes with smooth morphing animations, the whole nine yards. It was a flex. It looked great. I was proud of it.
But then I asked myself a simple question: “What am I actually building here?”
The Realization
My v1 site was a portfolio. It had an about section, experience timeline, skills grid, project cards, a writing section, and social links, all on one page with fancy animations. The blog was there, but it was secondary.
When I decided to pivot to weekly blogging, I looked at my Next.js setup and thought:
Framework JS shipped to browser: ~180KB
React runtime: ~45KB
Framer Motion: ~60KB
GSAP: ~30KB
Content that actually changes: some markdown text
🤔 Something doesn't add up.
I was shipping a full React runtime, animation libraries, and client-side hydration… to render static text that never changes. Every single blog post was being hydrated on the client, running JavaScript, setting up event listeners, for content that’s literally just paragraphs and code blocks.
That’s when it hit me: I was using a chainsaw to butter toast.
Why Not Just Keep Next.js?
Fair question. Next.js can do static sites. It has SSG, ISR, the whole deal. But here’s what bugged me:
1. The bundle tax
Even with static generation, Next.js ships React to the browser. Every page gets hydrated. For a blog post that’s just text and code blocks, that’s unnecessary JavaScript that the user’s browser has to download, parse, and execute.
2. The complexity overhead
My v1 had content-collections, next-mdx-remote, react-markdown, @wooorm/starry-night for syntax highlighting, remark-gfm, rehype… the dependency tree for just rendering markdown was wild.
// v1: Dependencies just for blog content
"@content-collections/core": "0.10.0",
"@content-collections/mdx": "0.2.2",
"@content-collections/next": "0.2.6",
"next-mdx-remote": "5.0.0",
"react-markdown": "10.1.0",
"remark-gfm": "4.0.1",
"rehype": "13.0.2",
"@mdx-js/react": "3.1.0",
"@wooorm/starry-night": "3.8.0",
"hast-util-to-html": "9.0.5"
3. The “everything is a component” trap
In React-land, even a simple blog post layout becomes a tree of components with state, effects, and client directives. My blog-post-layout.tsx was a client component importing react-markdown with custom renderers, wrapped in motion divs. For what? To display an article.
Enter Astro
I’d been hearing about Astro for a while. The pitch was simple: ship zero JavaScript by default. Only hydrate what actually needs interactivity. For a blog, that’s… almost nothing.
Here’s what sold me:
1. Zero JS by default
Astro renders everything to HTML at build time. No React runtime shipped. No hydration. A blog post page literally sends HTML and CSS to the browser. That’s it. The way the web was meant to work.
v1 (Next.js) blog post page: ~230KB JS
v2 (Astro) blog post page: ~0KB JS (just HTML + CSS)
Yes, zero JavaScript on a blog post page. The only JS on the entire site is a tiny inline script for the dark mode toggle.
2. Built-in content collections
Astro has content collections as a first-class feature. No third-party packages needed. Define a schema, drop MDX files in a folder, and you’re done.
// That's the entire content setup
const blog = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()),
published: z.boolean().default(false),
}),
});
Compare that to the three packages and custom transform functions I needed in Next.js.
3. Shiki built-in
Syntax highlighting just works. No installing starry-night or prism or configuring rehype plugins. Astro uses Shiki out of the box with dual theme support:
// astro.config.mjs
markdown: {
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
},
},
That’s it. Every code block in every blog post gets beautiful syntax highlighting with zero client-side JavaScript.
4. I can still use React (when I need it)
This was crucial. Astro’s “islands” architecture means I can drop in a React component when I actually need client-side interactivity. Everything else is static HTML.
<!-- Only this component ships JS, everything else is static -->
<InteractiveWidget client:load />
<!-- The rest of the page is zero-JS HTML -->
For my site, I ended up handling tag filtering and the sidebar toggle with small inline scripts instead of React. That’s even less JavaScript shipped.
5. View Transitions API
Astro has built-in support for the View Transitions API. Smooth page-to-page animations with zero JavaScript bundle cost. No Framer Motion. No GSAP. Just native browser APIs.
The Migration
The actual migration was surprisingly smooth:
- MDX posts: Copied all the files over. The frontmatter schema was almost identical, just had to remove some content-body
---separators that MDX interpreted as frontmatter delimiters. - Data: Extracted experience and skills data into simple TypeScript files.
- Styling: Kept Tailwind v4, same CSS variables, same color scheme.
- Dark mode: One inline script in the head. No
next-themespackage.
The whole thing took a day. The v1 had 15+ components and multiple npm packages for content. The v2 has Astro components (basically HTML with props) and barely any dependencies.
The Numbers
| Next.js v1 | Astro v2 | |
|---|---|---|
| Dependencies | 30+ | ~10 |
| JS shipped (blog post) | ~230KB | ~0KB |
| Build time | ~15s | ~5s |
| Lighthouse Performance | 85-90 | 99-100 |
| Framework complexity | High | Low |
What I Miss
I’ll be honest, there are things I miss:
- The morphing layout was genuinely cool. Switching between “original” and “swiss” design modes with smooth Framer Motion animations was a showpiece. But it was a portfolio flex, not a blogging feature.
- React’s component model is more powerful for complex UI. Astro components are simpler but that’s by design.
- The ecosystem: React has a package for everything. Astro’s ecosystem is smaller (but growing fast).
The Takeaway
The best framework is the one that fits your use case. Next.js is incredible for web applications: dashboards, e-commerce, SaaS products, anything with lots of interactivity and dynamic data. I’d pick it again in a heartbeat for those.
But for a content-first blog where the primary job is serving static text with good typography and fast page loads? Astro is purpose-built for exactly that.
if (site === 'blog') {
return 'astro';
} else if (site === 'app') {
return 'nextjs';
} else {
return 'it depends™';
}
The v1 portfolio is still alive as an archive, a reminder that sometimes the best engineering decision is knowing when to use a simpler tool.
Now if you’ll excuse me, I have a blog to write every week. And this time, the framework won’t be in my way.