Migrating from Next.js to Astro: A Complete Guide
Migrating from Next.js to Astro can significantly improve your website’s performance while maintaining the developer experience you love. This comprehensive guide will walk you through the entire migration process, from planning to deployment.
Why Migrate to Astro?
Performance Benefits
- Zero JavaScript by Default: Only ship JavaScript when needed
- Faster Build Times: Astro’s optimized build process
- Better Core Web Vitals: Improved LCP, FID, and CLS scores
- Smaller Bundle Sizes: Reduced client-side JavaScript
Developer Experience
- Component Islands: Use React, Vue, Svelte together
- Familiar Syntax: Similar to JSX with enhanced features
- Built-in Optimizations: Image optimization, CSS bundling
- TypeScript Support: First-class TypeScript integration
Pre-Migration Assessment
Analyze Your Current Next.js App
Before starting the migration, audit your existing application:
# Analyze bundle size
npx @next/bundle-analyzer
# Check dependencies
npm list --depth=0
# Review page structure
find pages -name "*.js" -o -name "*.tsx" | wc -l
Compatibility Checklist
- Static pages vs dynamic pages
- API routes usage
- Client-side routing requirements
- Third-party integrations
- Custom webpack configuration
- Middleware usage
Step 1: Project Setup
Initialize Astro Project
# Create new Astro project
npm create astro@latest my-astro-site
# Choose template
✔ How would you like to start your new project? › Just the basics
✔ Install dependencies? … Yes
✔ Initialize a new git repository? … Yes
✔ TypeScript? … Yes
Install React Integration
Since you’re migrating from Next.js, you’ll likely want to keep using React:
npx astro add react
npx astro add tailwind # If using Tailwind CSS
Project Structure Comparison
Next.js Structure:
pages/
├── _app.js
├── _document.js
├── index.js
├── about.js
└── api/
└── users.js
Astro Structure:
src/
├── layouts/
│ └── Layout.astro
├── pages/
│ ├── index.astro
│ └── about.astro
├── components/
└── content/
Step 2: Layout Migration
Convert _app.js to Layout
Next.js _app.js:
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return (
<div>
<header>My Site</header>
<Component {...pageProps} />
<footer>© 2024</footer>
</div>
)
}
export default MyApp
Astro Layout.astro:
---
export interface Props {
title: string;
}
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<header>My Site</header>
<main>
<slot />
</main>
<footer>© 2024</footer>
</body>
</html>
Step 3: Page Migration
Static Pages
Next.js page:
// pages/about.js
import Head from 'next/head'
export default function About() {
return (
<>
<Head>
<title>About Us</title>
</Head>
<div>
<h1>About Us</h1>
<p>Welcome to our company!</p>
</div>
</>
)
}
Astro page:
---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';
---
<Layout title="About Us">
<div>
<h1>About Us</h1>
<p>Welcome to our company!</p>
</div>
</Layout>
Dynamic Pages with getStaticProps
Next.js dynamic page:
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post } };
}
export async function getStaticPaths() {
const posts = await fetchAllPosts();
return {
paths: posts.map(post => ({ params: { slug: post.slug } })),
fallback: false
};
}
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Astro dynamic page:
---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/Layout.astro';
export async function getStaticPaths() {
const posts = await fetchAllPosts();
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
---
<Layout title={post.title}>
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
</Layout>
Step 4: Component Migration
Convert React Components
Most React components can be used directly in Astro with the React integration:
Create a React component:
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Use in Astro page:
---
// src/pages/interactive.astro
import Layout from '../layouts/Layout.astro';
import Counter from '../components/Counter.jsx';
---
<Layout title="Interactive Page">
<h1>Interactive Elements</h1>
<Counter client:load />
</Layout>
Client Directives
Astro’s client directives control when components hydrate:
client:load- Hydrate immediatelyclient:idle- Hydrate when browser is idleclient:visible- Hydrate when component is visibleclient:media- Hydrate based on media query
Step 5: Data Fetching Migration
Static Data Fetching
Next.js getStaticProps:
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data');
return { props: { data } };
}
Astro frontmatter:
---
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---
<Layout title="Data Page">
<div>{JSON.stringify(data)}</div>
</Layout>
Content Collections
For blog posts and similar content, use Astro’s Content Collections:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.date(),
tags: z.array(z.string()),
}),
});
export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
const allPosts = await getCollection('blog');
---
<Layout title="Blog">
<h1>Blog Posts</h1>
{allPosts.map(post => (
<article>
<h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.description}</p>
</article>
))}
</Layout>
Step 6: API Routes Migration
Simple API Routes
Next.js API route:
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello World' });
}
Astro API route:
// src/pages/api/hello.ts
export async function GET() {
return new Response(JSON.stringify({ message: 'Hello World' }), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}
Complex API Routes
// src/pages/api/users/[id].ts
export async function GET({ params }) {
const { id } = params;
const user = await fetchUser(id);
if (!user) {
return new Response(null, { status: 404 });
}
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }
});
}
export async function POST({ request }) {
const data = await request.json();
const user = await createUser(data);
return new Response(JSON.stringify(user), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
}
Step 7: Styling Migration
CSS Modules
Next.js CSS Modules:
/* styles/Home.module.css */
.container {
padding: 2rem;
}
import styles from '../styles/Home.module.css';
export default function Home() {
return <div className={styles.container}>Content</div>;
}
Astro Scoped Styles:
---
// src/pages/index.astro
---
<div class="container">Content</div>
<style>
.container {
padding: 2rem;
}
</style>
Global Styles
---
// src/layouts/Layout.astro
import '../styles/global.css';
---
<html>
<!-- Layout content -->
</html>
Step 8: Image Optimization
Next.js Image Component
Next.js:
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority
/>
Astro:
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<Image
src={heroImage}
alt="Hero image"
width={800}
height={600}
loading="eager"
/>
Step 9: Routing Migration
File-based Routing
Both Next.js and Astro use file-based routing, but with some differences:
Next.js:
pages/blog/[slug].js → /blog/:slug
pages/blog/[...slug].js → /blog/*
Astro:
src/pages/blog/[slug].astro → /blog/:slug
src/pages/blog/[...slug].astro → /blog/*
Dynamic Routes
---
// src/pages/products/[category]/[id].astro
export async function getStaticPaths() {
return [
{ params: { category: 'electronics', id: '1' } },
{ params: { category: 'books', id: '2' } },
];
}
const { category, id } = Astro.params;
---
<Layout title={`Product ${id} in ${category}`}>
<h1>Product {id}</h1>
<p>Category: {category}</p>
</Layout>
Step 10: Environment Variables
Configuration
Next.js:
// next.config.js
module.exports = {
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
}
Astro:
// astro.config.mjs
export default defineConfig({
// Configuration
});
Usage
---
// Access environment variables
const apiKey = import.meta.env.PUBLIC_API_KEY;
const secretKey = import.meta.env.SECRET_KEY; // Server-side only
---
Step 11: Testing Migration
Component Testing
// tests/components/Counter.test.jsx
import { render, fireEvent } from '@testing-library/react';
import Counter from '../src/components/Counter.jsx';
test('counter increments', () => {
const { getByText } = render(<Counter />);
const button = getByText('Increment');
fireEvent.click(button);
expect(getByText('Count: 1')).toBeInTheDocument();
});
Page Testing
// tests/pages/about.test.js
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import About from '../src/pages/about.astro';
test('about page renders', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(About);
expect(result).toContain('About Us');
});
Step 12: Performance Optimization
Bundle Analysis
# Analyze Astro build
npm run build
npx astro build --analyze
Optimization Checklist
- Remove unused dependencies
- Optimize images with Astro’s Image component
- Use client directives appropriately
- Implement proper caching headers
- Minimize client-side JavaScript
Step 13: Deployment
Vercel Deployment
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel(),
});
Netlify Deployment
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';
export default defineConfig({
output: 'server',
adapter: netlify(),
});
Migration Checklist
Pre-Migration
- Audit current Next.js application
- Identify static vs dynamic pages
- List all dependencies
- Plan migration strategy
During Migration
- Set up Astro project
- Migrate layouts and components
- Convert pages one by one
- Update routing and navigation
- Migrate API routes
- Test functionality
Post-Migration
- Performance testing
- SEO verification
- Cross-browser testing
- Deploy to staging
- Monitor Core Web Vitals
- Update documentation
Common Pitfalls and Solutions
1. Client-Side State Management
Problem: Global state not working across pages Solution: Use Astro’s persistent state or move to client-side routing
2. Dynamic Imports
Problem: Dynamic imports behaving differently Solution: Use Astro’s dynamic imports or component islands
3. CSS-in-JS Libraries
Problem: Styled-components not working Solution: Use Astro’s scoped styles or CSS modules
Performance Comparison
After migration, you should see improvements in:
- Lighthouse Performance: 90+ scores
- Bundle Size: 50-80% reduction
- Time to Interactive: Significant improvement
- Core Web Vitals: Better LCP, FID, CLS scores
Conclusion
Migrating from Next.js to Astro can significantly improve your website’s performance while maintaining developer productivity. The key is to plan carefully, migrate incrementally, and test thoroughly.
Remember:
- Start with static pages first
- Use React components where interactivity is needed
- Leverage Astro’s client directives for optimal performance
- Test each step of the migration
- Monitor performance improvements
The migration effort is worth it for the performance gains and improved user experience that Astro provides.