Back to blog

Why I Switched from Next.js to Astro

4 min read Web Development

I built ricsmo.com and course.coach with Next.js. Each one took less than a week. Both are content sites. Neither needs a server, a database, or client-side interactivity beyond a reading progress bar and a table of contents.

But Next.js ships like you need all of those things.

The Problem

Next.js static export (output: "export") has a fundamental mismatch with content sites. Every page ships a React runtime, hydration code, and RSC (React Server Components) payload inline in the HTML. Even with zero interactive components.

Here’s what a typical blog post looked like in the Next.js build:

  • Total HTML: ~52 KB
  • Inline React scripts: ~28 KB (54% of the page)
  • Actual content text: ~4.7 KB (9% text-to-HTML ratio)

The framework overhead was larger than the content. On pages with short content like the homepage or the /uses page, the ratio dropped to 3-4%. An SEO audit tool flagged every page for having a text-to-HTML ratio below 10%.

That’s not a fixable optimization problem. That’s the wrong tool for the job.

What Broke My Patience

The tipping point wasn’t performance. It was the constant fighting with Next.js limitations that only affect static sites.

Route handlers don’t work. Next.js static export can’t generate robots.ts, sitemap.ts, or RSS route handlers at build time. The build fails with a cryptic error about dynamic routes. The fix is to rename .ts files to .mjs and use npx tsx to compile them, which feels like duct tape on top of duct tape.

next start doesn’t work. You can’t run next start with output: "export". You have to use npx serve out instead, which has its own quirks like binding to IPv6 by default on macOS.

Blog index renders as /blog.html not /blog/index.html. So /blog URL doesn’t resolve without manually copying the file after every build.

Root layout canonical override. If your root layout sets a canonical URL, every child page inherits it. Google thinks every page is a duplicate of the homepage. Took me a full debugging session to find that one.

These aren’t edge cases. They’re all things you run into when building a normal blog with static export. The Next.js docs barely mention static export. It’s an afterthought in a framework designed for server-rendered apps.

The Numbers

After rebuilding both sites with Astro:

Next.jsAstro
JS chunks15 files, 761 KB0
Total site13.1 MBSmaller
Text-to-HTML ratio3-16%50%+
Interactive JSReact runtime on every page~2 KB inline (progress bar + TOC only)
Route handlersBroken, needed workaroundsBuilt-in (sitemap, RSS)

The biggest win isn’t any single metric. It’s that Astro defaults to zero JavaScript. You opt in to interactivity per component, not the other way around. For a blog, that means every page is just HTML and CSS.

What Astro Gets Right

Content collections are first-class. Define your schema in a config file, put your MDX in src/content/blog/, and everything else (typing, slugs, dates, frontmatter validation) is handled.

The build just works. Sitemap generation is a built-in integration. No separate scripts, no npx tsx, no build pipeline juggling.

No hydration tax. My blog posts have a reading progress bar and a table of contents with active heading tracking. In Next.js, those required React components with useState and useEffect, which means shipping the React runtime to every page. In Astro, they’re ~20 lines of vanilla JS in an inline <script> tag. No framework needed.

output: 'static' is the default. Not an afterthought. Not a config flag that disables half the framework.

What I Lost

Not much. The React components (Header, Footer, BlogCard, etc.) ported easily. Astro can render React components, so I kept the ones I wanted and replaced the interactive ones with plain JS.

The migration took a weekend. Most of that was adjusting import paths and replacing Next.js-specific APIs (next/image, next/link, next/script) with standard HTML equivalents.

The Lesson

Next.js is a great framework for server-rendered applications. If you need API routes, server actions, or dynamic data fetching at request time, it’s hard to beat.

But if you’re building a blog, a docs site, or a marketing page, the React runtime is dead weight. You’re paying for capabilities you don’t use and fighting against limitations that exist only because the framework assumes you need them.

Astro doesn’t try to be everything. It’s a static site generator that handles content well and lets you add interactivity where you actually need it. For the kind of sites I build, that’s the right tradeoff.

If you’re thinking about building or rebuilding a content site, let’s talk.

Share

More writing