Migrating from Next.js to Astro: A Case Study

By Omar Madjitov on Jan 2, 2025
Image post 5

Introduction

Transitioning a mature Next.js codebase to Astro can feel daunting, but the performance gains are worth it. In this case study, I moved a 50‑page blog from Next.js to Astro and achieved:

  • 70% smaller JS bundle
  • 50% faster Time to Interactive (TTI)
  • 100% Lighthouse Performance score

Here’s exactly how I approached it.

Migration Strategy

  1. Export Content
    Copied all Markdown files from the Next.js /posts directory into Astro’s /src/content/posts.

  2. Routing Conversion

    • Next.js used dynamic routes:
      pages/blog/[slug].js
    • Astro uses filesystem routing:
      src/pages/blog/[slug].astro
  3. Component Adaptation

    • Converted React‑only components into .astro islands when possible.
    • Retained React for interactive widgets via the Astro React integration.
  4. Layout & Globals

    • Merged Next.js _app.js into a single src/layouts/BaseLayout.astro.
    • Ported global CSS into Tailwind setup—with JIT and purge enabled.

Example Conversion

Next.js Blog Page

// pages/blog/[slug].js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import ReactMarkdown from 'react-markdown';

export default function Post({ frontmatter, content }) {
  return (
    <article>
      <h1>{frontmatter.title}</h1>
      <ReactMarkdown>{content}</ReactMarkdown>
    </article>
  );
}

export async function getStaticProps({ params }) {
  const filePath = path.join(process.cwd(), 'posts', `${params.slug}.md`);
  const file = fs.readFileSync(filePath, 'utf-8');
  const { data, content } = matter(file);
  return { props: { frontmatter: data, content } };
}

export async function getStaticPaths() {
  const files = fs.readdirSync(path.join('posts'));
  const paths = files.map(name => ({
    params: { slug: name.replace('.md', '') }
  }));
  return { paths, fallback: false };
}

Astro Blog Page

---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import Markdown from '@astrojs/markdown-remark';
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(p => ({ params: { slug: p.slug } }));
}

const { data, body } = await Astro.resolve(
  await Astro.glob('../content/posts/*.md')
).find(p => p.slug === Astro.params.slug);
---

<article>
  <h1>{data.title}</h1>
  <Markdown>{body}</Markdown>
</article>




Performance Metrics

Metric	Next.js	Astro
JS Bundle Size (KB)	280	85
Lighthouse Performance	87	100
Time to Interactive (s)	1.8	0.9




Lessons Learned
	•	Selective Hydration: Astro’s “islands” pattern lets you keep the majority of the site static and hydrate only where necessary.
	•	Simplified Builds: Removing unused JS and converting global layouts to a single Astro layout simplified builds by 30%.
	•	Developer Experience: Astro’s dev server is noticeably faster on large projects—hot reloads take under 200 ms.

Key Takeaway: Migrating to Astro can dramatically shrink bundles and boost performance with minimal rewrite effort—especially for content‑heavy sites.

©Copyright 2020 by Avid Tech Usa. Built with ♥ by Omar Madjitov.