After running my personal portfolio and blog on Next.js for over two years, I made the decision to migrate to Astro. This wasn’t a decision I took lightly, but after experiencing the benefits firsthand, I can confidently say it was the right choice for my use case. Here’s why I made the switch and what I learned along the way.
Table of contents
Open Table of contents
The Context: What I Had Before
My previous setup was a Next.js application with:
- Static site generation (SSG) for blog posts
- MDX for content authoring
- Tailwind CSS for styling
- Deployed on Vercel
- A mix of static pages and dynamic portfolio sections
While this setup worked well, I started noticing some pain points as my content grew and my needs evolved.
The Problems with My Next.js Setup
1. Bundle Size and Performance
Despite using SSG, my Next.js site was shipping unnecessary JavaScript to the browser. Even simple blog posts were loading React’s runtime and hydration code, which felt excessive for content that was essentially static.
The lighthouse scores were good, but I knew they could be better without the JavaScript overhead.
2. Complexity for Simple Content
For a blog that was 90% static content, the React component model felt like overkill. I found myself writing components for simple layouts that could have been plain HTML.
Another issue is the underutilization of certain Next.js features like API routes… while they are great for certain applications, such as BFF, for a micro blog they were almost entirely useless.
3. Build Times
As my content library grew, Next.js build times started to increase noticeably. The framework was doing a lot of work that wasn’t necessary for my static content.
4. SEO and Meta Management
While Next.js has good SEO capabilities, managing meta tags and Open Graph images across different post types required more boilerplate than I wanted to maintain.
Why Astro Was the Perfect Fit
1. Zero JavaScript by Default
Astro’s “islands architecture” means that by default, no JavaScript is shipped to the browser unless explicitly needed. For blog posts, this means:
---
// This runs at build time only
const { post } = Astro.props;
---
<article>
<h1>{post.title}</h1>
<div set:html={post.content} />
</article>
The result? Significantly smaller bundle sizes and faster page loads.
2. Better Performance Out of the Box
My Lighthouse scores improved across the board:
- Performance: 95+ (up from 85-90)
- First Contentful Paint: Reduced by 40%
- Largest Contentful Paint: Reduced by 35%
3. Simplified Content Management
Astro’s content collections made managing blog posts incredibly straightforward:
// content.config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDatetime: z.date(),
author: z.string().default('Edgar Montano'),
tags: z.array(z.string()).default(['others']),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
4. Excellent Developer Experience
- Hot reloading is lightning fast
- TypeScript support is first-class
- Markdown and MDX work seamlessly
- Component flexibility - I can still use React, Vue, or Svelte when needed
5. Built-in Optimizations
Astro includes many optimizations out of the box:
- Automatic image optimization
- CSS and JavaScript minification
- Static asset optimization
- Automatic sitemap generation
The Migration Process
1. Content Migration
The easiest part was migrating my Markdown content. Astro’s content collections made this straightforward:
# Old structure (Next.js)
/pages/blog/[slug].js
/content/posts/my-post.mdx
# New structure (Astro)
/src/pages/posts/[...slug].astro
/src/data/blog/my-post.md
2. Component Migration
I rewrote my React components as Astro components. Most were simple enough that this was just removing unnecessary state and effects:
<!-- Before (React/Next.js) -->
import { useState } from 'react';
export default function BlogCard({ post }) {
return (
<div className="blog-card">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</div>
);
}
<!-- After (Astro) -->
---
const { post } = Astro.props;
---
<div class="blog-card">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</div>
3. Routing Updates
Astro’s file-based routing was similar to Next.js, so most routes mapped directly. The main difference was using .astro
files instead of .js/.tsx
.
4. Styling Migration
Since I was already using Tailwind CSS, the styling migration was minimal. Astro has excellent Tailwind support out of the box.
What I Gained
1. Performance
The performance improvements were immediately noticeable:
- reduction in bundle size
- faster page load times
- Perfect Lighthouse performance scores
- Improved Core Web Vitals
2. Simplicity
My codebase became significantly simpler:
- Fewer dependencies
- Less boilerplate code
- Cleaner component architecture
- Easier content management
3. Better SEO
Astro’s built-in SEO features made optimization easier:
- Automatic meta tag generation
- Built-in sitemap support
- Excellent structured data handling
- Dynamic OG image generation
4. Faster Development
- Build times reduced by 60%
- Hot reloading is nearly instantaneous
- Less context switching between different concepts
Conclusion
While Next.js is an excellent framework for dynamic applications, Astro’s focus on static content and performance made it the perfect fit for my use case.
The migration process was smoother than expected, and the performance benefits were immediate and substantial. If you’re running a content-heavy site on Next.js and feeling like you’re over-engineering your solution, consider giving Astro a try.