Article
Migrating from Next.js to Astro
Three years after I left Gatsby for Next.js, I left Next.js for Astro.
This is a short reflection on why, what the migration looked like, and what changed in the numbers. This site is the migration — heyeddsoftwares.com.au is now an Astro site; until last week it was a Next.js 14 app.
What changed in Next.js (and what didn’t)
In 2024 I wrote that Next.js was getting harder to use. The complexity has compounded since:
- App Router became the default. The old Pages Router is now effectively deprecated for new projects. The migration is non- trivial — they’re functionally different programming models.
- Server Components. Conceptually clean but practically tricky. The boundary between server and client components is leaky in real codebases.
- Caching surfaces.
revalidate,dynamic = 'force-static',fetch({ next: { revalidate } }),unstable_cache,unstable_noStore,noStore(). There are now half a dozen ways to control caching, and the interactions between them have surprised me three times this year. - Build steps got slower again. Gatsby’s builds were slow because of plugins; Next.js’s are slow because of the App Router’s compilation surface.
next/imagekeeps growing. It now wants configuration for remote images, on-demand transforms, sharp installation. For a static content site, it’s overkill.
For a content-driven site — case studies, articles, services pages — I was using maybe 5% of Next.js. The rest was tax.
Why Astro
I’d been watching Astro for a year. The pitch is clean: static-first, framework-agnostic, content-driven.
The things that pulled me over:
- Zero JavaScript by default. Every Astro page ships zero JS unless you explicitly opt in. For a content site, that’s the right default.
- Content collections. Native Markdown / MDX support with Zod- validated frontmatter. The data layer I missed from Gatsby, but lighter and typed.
- No App Router / Pages Router fork. One way to build pages: file-
based routing with frontmatter and
getStaticPaths-style functions on dynamic routes. - Component model is plain HTML + scoped CSS. No
'use client'boundaries. No Server Component leakage. Just templates that render once at build time. - Astro Islands for the rare interactive bit — opt-in React / Vue / Svelte components hydrated only where needed. My islands so far on this site: zero.
The migration
Migrating four sites’ worth of work — a personal site, a product site, a glossary, and this one — took about two weeks of part-time effort.
What was easy:
- Markdown content. Drop the
.mdfiles in.contents/<collection>/and define a Zod schema. Done. - Routing. File-based, like Next.js Pages Router. Zero learning curve.
- Layouts. A single
Base.astrowraps every page; sections compose. - Styles. Native CSS variables + scoped
<style>blocks per component. No Tailwind required (though it’s available). On this site I deleted ~3,000 lines of Tailwind config and SCSS.
What was harder:
- React component reuse. I’d built a handful of bespoke React
components (custom search, animated hero). For some I rewrote in
Astro template syntax (better performance); for others I kept as
React islands (
client:visible). - Image handling. Astro’s
Imagecomponent is good but the conventions differ fromnext/image. Took an hour of doc-reading to internalise. - Sitemap / RSS. First-party Astro integrations exist
(
@astrojs/sitemap,@astrojs/rss) but the API differs fromnext-sitemap. Quick wins after the doc tour.
What I dropped entirely:
- 30+ npm dependencies (Flowbite, GSAP, AOS, Framer Motion,
react-router-dom, react-native-web, formik, axios, lodash,
classnames, clsx, …). The new stack:
astroplus@astrojs/sitemap. That’s it. - The
LayoutNoSSRworkaround I’d written to dodge a Next.js hydration bug. Astro doesn’t hydrate at all by default, so the bug class doesn’t apply.
Numbers
A back-of-envelope comparison after the migration on this site:
| Next.js 14 | Astro 5 | |
|---|---|---|
| Direct dependencies | 30+ | 2 |
| Cold build time | ~90 s | ~3 s |
| First-paint (4G) | ~1.4 s | ~0.6 s |
| Lighthouse perf | 78 | 99 |
| Total JS shipped (home) | ~280 KB | 0 KB |
These are my numbers, on my sites. Yours will vary. But the direction is consistent — the stack got smaller and the site got faster, with no UX feature loss for content-driven pages.
What I’d tell myself in May 2026
- Don’t migrate just to migrate. I waited until I had a real reason. The “I want to” reason is fine; the “this stack is annoying me” reason is fine. Don’t migrate because Hacker News is excited about something.
- Migrate one site first. I migrated a small personal site before touching any of the bigger ones. The lessons from that informed every later migration.
- Save the URLs. Cool URLs don’t change. Map every old route to a new one before you cut over. SEO doesn’t forgive lazy redirects.
- Don’t expect Islands to do much. Most of my “interactive” components turned out to be unnecessary — they were animations or hovers that scoped CSS could handle.
Should you migrate?
For content sites — portfolios, blogs, marketing pages, docs sites — yes. The Astro story is so much simpler than Next.js’s that it pays for itself within the migration window.
For dashboards, complex apps, or anything with heavy client-state interaction: stay on Next.js, or look at Remix / TanStack Start. Astro is happiest when the page is mostly content.
This site is the proof. Three years of case studies and articles, all served as static HTML with effectively no JavaScript shipped. The fastest version of itself it’s ever been.