Skip to content

Migrating this site to Astro with AI

Next.js -> Astro, mobile Lighthouse scores from the 80s to ~100

August 26, 2026

I migrated this site from Next.js to Astro with Claude Code (Fable/High) in less than a day. Along the way I also fixed several issues, updated dependencies, and improved the code. The Lighthouse scores improved from 80-95 to 99-100. I think some of what I did and learned could be applicable in other migrations or situations. This is somewhat inspired by Bun’s Zig to Rust migration. In this case it’s frontend web dev, a tiny codebase, and I didn’t start out with tons of tests. Using AI I was able to setup visual and performance testing, experiment with several frameworks and optimizations, and do the migration quickly and with no regressions.

Process

  • Build a visual regression testing setup.
    • Prompt: “I want to add visual regress / snapshot testing to this repo. I don’t want to have to use an external service for it. Come up with a plan for me”.
    • Framework agnostic with Playwright.
    • 125 snapshot tests, every page, light/dark, mobile/laptop.
    • I looked into using Git LFS with R2 but ended up just checking them in since they’re around 35 MB.
    • The initial one-shot implementation of the snapshot testing took 5 minutes to run.
    • This seemed slow, so I prompted the AI to improve it. It found that it had initially used waits that were not as efficient as possible. By using different waiting strategies, runtime came down to around 1 minute. I wasn’t totally happy with this code since the waiting seemed more complicated than needed. Instead of just trying to optimize the visual regression runtime, I could have told the agent it could modify the code to make it more testable.
    • I added some specific visual regression testing of the sandbox setup.
  • Build a performance testing setup.
    • Prompt: “Come up with a plan for measuring some metrics about the site. I want to be able to compare these metrics between builds”
    • Collect metrics like hydration time as well as Lighthouse metrics.
    • Measure navigation time between pages.
    • This runs several times to get stable results.
  • Migrate Next 13 -> Next 16.
    • Mixed results: the sandbox post got lighter (274 KB to 173 KB of JS) but the home page a little heavier, and the Lighthouse scores went from 95/80 to 93/88.
  • Migrate from Next Pages Router -> Next App Router.
    • Minimal improvement in some metrics, small increase in bundle size.
  • Self-host the fonts instead of using next/font.
    • Total transfer on the home page went from 351 KB to 248 KB and LCP from 2.7 s to 2.0 s; the App Router row in the tables is measured after this.
  • Migrate from Next App Router -> TanStack Start with React Server Components.
    • Hydration time was about the same, but first paint got slower (FCP roughly doubled).
  • Migrate from Next App Router -> Astro
    • Prompt: “Let’s try switching to Astro - https://docs.astro.build/en/install-and-setup/
    • The site is just a blog; it didn’t really need Next.js. All the content is static and can be rendered at build time.
    • This boosted Lighthouse scores to almost 100.
    • After some more improvements, scores landed at 100 on the home page and 99 on posts.

Results

Mobile, medians over repeated runs. JS, total, and hydration are measured directly in Chrome against a local static server (hydration is the time until a performance.mark call). FCP, LCP, and score are Lighthouse with mobile throttling. Mobile + throttling increases numbers a lot and makes it almost impossible to hit 100 on a page with any complexity.

Home page:

StageJSTotalHydratedFCPLCPScore
Next 13, Pages Router (start)127312226755287295
Next 16, Pages Router152337244906316393
Next 16, App Router163248253755196499
TanStack Start, RSC1181992941806225697
Astro1.4110439021655100

A post with code sandboxes:

StageJSTotalHydratedFCPLCPScore
Next 13, Pages Router (start)2745163521207405780
Next 16, Pages Router1734343981656375988
Next 16, App Router1674074041055286095
TanStack Start, RSC1213474561953285394
Astro4.42172761429180399
  • JS: kilobytes of gzipped JavaScript the page itself downloads by the time it’s interactive. The sandbox previews run in iframes and aren’t counted.
  • Total: kilobytes of everything the page downloads, gzipped: HTML, CSS, fonts, images, JS.
  • Hydrated: milliseconds from navigation start to the site’s performance.mark('app:hydrated'), when its scripts have run and the page is interactive. Measured in Chrome with a 4x CPU slowdown and no network throttling against a local static server, median of 5 runs.
  • FCP, LCP, Score: First Contentful Paint and Largest Contentful Paint in milliseconds, and the performance score, from Lighthouse’s simulated mobile (slow 4G, 4x CPU slowdown), median of 3 runs.

Navigating from the home page to a post, mobile, 4x CPU slowdown. Next and TanStack navigate client side; Astro loads the new page:

StageTime
Next 13, Pages Router (start)161
Next 16, Pages Router142
Next 16, App Router158
TanStack Start, RSC66
Astro128
  • Time: milliseconds from clicking the link on the home page until the post is rendered: the site’s app:navigated mark for the client-side routers, the new document’s load for Astro.

It’s interesting that TanStack outperforms in client-side navigation, which is in line with its focus on client-side apps.

What went well

  • Building a framework-agnostic snapshot testing and performance testing setup first.
  • Getting the agent to make the testing setup faster.
    • If I hadn’t done this, every iteration the agent did would take at least an extra 4 minutes.
    • If this whole thing took a couple hundred iterations, that could add up to an extra 13 hours. So my one prompt of “the testing seems slow, can you make it faster?” paid off a lot.
    • Just because you’re not the one triggering the tests, or running the build, doesn’t mean you shouldn’t try to make them faster, because it still affects how long things take.
    • The agents don’t have much of a sense of time, and will just put up with things being slow and not mention it.
    • Agents also don’t hesitate to run a fresh build or test even when they take time, which is often more correct (a human might just keep coding without building or running tests), but exacerbates the issue even more.
  • Using framework-agnostic testing and benchmarking that doesn’t rely on implementation details.
    • If I used React Testing Library and the React Profiler (kind of contrived, I don’t think people just rely on the React profiler for benchmarking, but imagine for the sake of this line of thinking) for testing and benchmarking, then switching frameworks would require rewriting a significant chunk of test and benchmark code which would make things more error-prone, and the benchmarking less comparable.
  • Experimenting with different ideas.
    • An agent can easily catch regressions using the testing setup so it’s easy to have it try new big changes without much input or risk.
    • I initially thought TanStack was going to be a good move, but it seemed like it performed slightly worse for this use case.
    • A simple blog like this is very suitable for RSC, which is probably why Next.js won since its RSC implementation is more mature.
    • It’s possible that I could have made TanStack beat Next.js with more effort, maybe by switching off RSCs, but I think it would have only been marginal, compared to Astro clearly winning in terms of perf.
    • Without AI, comparing frameworks is usually confined to smaller synthetic comparisons. With this setup I was able to compare four (Next Pages Router, Next App Router, TanStack, Astro) complete, pixel-accurate versions of the whole site within a few hours. Without AI the comparisons done would be less thorough, and you’d be contending with sunk-cost thinking if you did do a whole migration.

What could have gone better

  • Some regressions crept in: the agent was often good at reading visual diffs, but it missed some spacing bugs.
  • I also missed the spacing bugs because my git client doesn’t expand the images to 100% zoom, and I couldn’t see them, so I definitely need a better image-diff review UI for these full page snapshot tests.
  • The agent and I both thought a spacing change was minor noise when it was actually a bug, and I only noticed on the actual deployed site.
  • Given what I mentioned regarding the snapshot test duration impacting iteration time, it might have been worthwhile to try to get the time down even more.
  • It’s also worth improving build times for the same reason.
    • After the migration to Astro, I asked the agent to investigate build time, and it found that disabling loading system fonts when generating OG images reduced build time from 10s to 3s. We didn’t do a ton of iteration after migrating to Astro, but it will help going forward.

Conclusions

Developers know that this way of migrating things is the right way to do it. They know they should keep behaviour and appearance identical when migrating, and that thorough experimentation without succumbing to sunk-cost thinking is ideal. Developers are human though, and these things often get tossed aside in the real world. With AI we can do things more correctly and get better results. Without AI we lacked the time and the mental fortitude; now, with AI, we can do things the way we know we should.

The one downside I can see of AI in this case is that it let me keep the exact content and design of this site, down to the pixel, when without it I might have thrown things away and made something more creative. On the other hand, now that everything is migrated and updated, I’m in a good place to make design changes.


Dylan Vann
Software developer.

Subscribed!
You should receive an email with a confirmation link!
Or follow me on X for more stuff like this.