A content site that did not need Next.js looks the same on day one as a content site that did. The mismatch shows up at month three — slower builds, bigger bundles, more deploy issues, and a CMS-to-rebuild dance that should have been a static export. Below are the actual benchmark commands and the migration plan if you confirm the mismatch.
Background
Next.js is optimized for hybrid app-and-content surfaces. When you have no app, no per-user state, and the dynamic parts are limited to search and a contact form, every advanced Next.js feature is either unused or actively in your way. Astro, Hugo, Eleventy, or even raw HTML deliver smaller bundles and simpler mental models.
How to tell
- You are writing zero
'use client'components after three months. - Your
next buildtakes longer than your last three Astro projects combined. - You have never used a server action, route handler, or middleware.
- Your hosting bill is dominated by bandwidth, not function execution — meaning you are paying Vercel for things Cloudflare Pages or Firebase Hosting would do cheaper.
- The Lighthouse JS bundle warning shows you shipping 200 KB+ on a blog post.
Quick verdict
If three or more of the signals above match, you are using Next.js as a static site generator with extra steps. Migrate to Astro for the same DX with smaller output, or to Hugo / Eleventy if you do not need a component model at all.
Before you start
- Document your current build time + LCP + bundle size before doing anything.
- Pick the most representative page (typical article) for the comparison.
- Set aside a half-day to do a parallel Astro spike.
Step by step
- Audit dynamic surfaces. A grep tells you the truth:
# count client components, server actions, middleware, route handlers
grep -rl "^'use client'" app/ | wc -l
grep -rl "^'use server'" app/ | wc -l
ls middleware.* 2>/dev/null
find app -name 'route.ts' -o -name 'route.tsx' | wc -l
A content site should have nearly zero 'use server', zero middleware.*, and very few route.ts.
- Benchmark current state. Build time + bundle:
time npm run build
# real: ?m ?s
# What is shipped on a typical article?
ls -lh .next/static/chunks/ | sort -k5 -h | tail
# the largest chunks loaded on a page
- Lighthouse on a typical article:
npx lighthouse https://yourdomain.com/articles/sample-slug/ \
--only-categories=performance,seo \
--chrome-flags="--headless" --quiet --output=json --output-path=./lh-next.json
jq '.audits."largest-contentful-paint".numericValue,
.audits."total-blocking-time".numericValue,
.audits."total-byte-weight".numericValue,
.audits."unused-javascript".details.overallSavingsBytes' lh-next.json
Record LCP (ms), TBT (ms), total bytes, unused JS.
- Spin up a parallel Astro project. Port one article + the index page. From scratch:
npm create astro@latest blog-spike -- --template minimal --install --yes
cd blog-spike
npm install @astrojs/mdx @astrojs/sitemap
# add astro.config.mjs with site, sitemap; copy one .mdx article in
npm run build
ls dist/articles/sample-slug/ # confirm output
- Compare:
# Astro build time
time npm run build
# Astro bundle on the same article URL via lighthouse
npx lighthouse http://localhost:4321/articles/sample-slug/ \
--only-categories=performance --chrome-flags="--headless" --output=json \
--output-path=./lh-astro.json
jq '.audits."largest-contentful-paint".numericValue,
.audits."total-byte-weight".numericValue' lh-astro.json
Astro content pages should ship near-zero JS and total byte weight should drop substantially.
- Plan the migration in phases. A useful order:
Phase 1 (1-2 weeks): Astro project parity for the blog
Phase 2 (1 week): Marketing pages migrated
Phase 3 (3-5 days): 301 redirects from old Next.js URLs to new
Phase 4 (1 day): Cut DNS / hosting; freeze Next.js repo
- 301 redirects via the new host’s config. Vercel
vercel.json:
{
"redirects": [
{ "source": "/old/(.*)", "destination": "/articles/$1", "permanent": true }
]
}
Or _redirects on Cloudflare Pages / Netlify:
/old/* /articles/:splat 301
- Verify SEO post-launch. Search Console URL Inspection + Performance comparison after 2-4 weeks. Average position should hold or improve; LCP/INP should improve materially.
Implementation checklist
- Inventory + benchmarks recorded before any migration.
- Astro spike completed with measurable Lighthouse improvement.
- Migration plan written with phases and reversal path.
- 301 redirects mapped 1:1 from old URL set to new.
- Old Next.js repo frozen, not deleted, for at least 30 days.
After-launch verification
- LCP drops by ≥ 30% on typical article pages.
- Total bytes drop by 60%+ on article pages with no client islands.
- Search Console “Indexed” count returns to baseline within 4-8 weeks.
- No 404s in Search Console for old URLs (301s carrying them).
Common pitfalls
- Migrating because of one slow build, then realizing the slowness was a bad image pipeline that any framework would have suffered.
- Picking Astro and then writing every island as
client:load— you have rebuilt Next.js in slow motion. - Underestimating the URL migration cost — every old URL needs a 301 to the new one, or you lose rankings.
- Switching tooling mid-launch — pick one and ship, change later if the data supports it.
- Deleting the Next.js repo too early — keep it 30+ days as a rollback option.
FAQ
- Will I lose SEO if I migrate?: Only if you change URLs without 301s, or if Core Web Vitals regress. A well-planned Next.js to Astro move usually improves rankings within a month.
- Is Astro really faster than Next.js for content?: Default-zero-JS makes a measurable LCP and INP difference for content pages. Builds are typically faster too.
- Can I keep MDX during migration?: Yes. Astro supports MDX natively. Most frontmatter and components port with minor edits.
- What about hosting?: Astro static output runs anywhere — Cloudflare Pages, Firebase Hosting, Vercel, Netlify, S3. You stop paying for SSR you were not using.
- How long does a typical migration take?: For a 100-200 article site, 2-4 weeks part-time. Most of the time is content QA and redirects, not framework code.