Skip to content

Migrating from Next.js to Astro with AI

Mobile Lighthouse score from 85 to 100

August 26, 2026
AI-assisted website changes verified by snapshots and optimized to a performance score of 100.

I migrated this site from Next.js to Astro with Claude Code (Fable/High) in less than a day and mobile Lighthouse scores improved from 85-98 to 100. Along the way I also fixed several issues, updated dependencies, and improved the code.

This is inspired by Bun’s Zig to Rust migration. In this case it’s frontend web dev, a tiny codebase, the most complicated part of which is a self-hosted, client-side code sandbox setup. I also didn’t start with many tests.

Using AI I was able to set up visual and performance testing, experiment with several frameworks and optimizations, and do the migration quickly and with no regressions.

Process

  • Add visual regression testing.
    • Prompt: “I want to add visual regression 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 prompted the agent to improve the speed of the visual regression tests and it did so by using more precise wait methods, which cut the time by around a minute.
  • Add performance testing.
    • 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. I later swapped Lighthouse for a separate testing setup based on real metrics with CPU slowdown and network throttling, more on that later in the article.
    • Measure navigation time between pages.
  • Add functional testing.
    • Playwright checks of links, feeds and metadata, plus some Vitest browser testing of playground components.
  • Upgrade Next 13 → Next 16.
    • Mixed results: the sandbox post got lighter (271 KB to 169 KB of JS) but the home page a little heavier, and the Lighthouse scores went from 98 to 97 on the home page, and 85 to 94 on the sandbox post.
  • Migrate Next Pages Router → Next App Router.
    • Slightly more JavaScript, otherwise much the same.
  • Self-host the fonts instead of using next/font.
    • This cut the home page’s transfer by about a third and its LCP by a quarter; the App Router row in the tables is measured after this.
  • Migrate Next App Router → TanStack Start with React Server Components.
    • Half the transfer, hydrated a second sooner, first paint sooner too.
  • Migrate 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 got Lighthouse scores to 100 for home and 99 for sandbox.
    • A later redesign swapped to lighter font files and dropped icon CSS. That change brought all scores to 100.

Results

Chrome loads each build over a shaped slow 4G link (150 ms round trip, 1.6 Mbps down) with a 4x CPU slowdown, medians of 11 loads. Every build is served using HTTP/2 with TLS. Only the score is Lighthouse’s.

Mobile + throttling increases these numbers a lot and makes it difficult to hit a Lighthouse score of 100 on a page with any complexity. Many sites that claim they hit 100 on Lighthouse only do so on desktop.

Home page

JavaScript KB gzipped
050100150AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)1.2 KB117 KB161 KB148 KB125 KB
FCP / LCP ms
05001,0001,500AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)988 ms1,480 ms1,540 ms1,648 ms1,752 ms
JavaScript KB gzipped
050100150AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)1.2 KB117 KB161 KB148 KB125 KB
FCP / LCP ms
05001,0001,500AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)988 ms1,480 ms1,540 ms1,648 ms1,752 ms
StageJSTotalHydratedFCP / LCPScore
Next 13 (Pages)125 KB308 KB2,523 ms1,752 ms98
Next 16 (Pages)148 KB332 KB2,762 ms1,648 ms97
Next 16 (App)161 KB347 KB2,800 ms1,540 ms99
TanStack Start117 KB197 KB1,739 ms1,480 ms99
Astro1.2 KB91 KB978 ms988 ms100

Post page with sandboxes

Lighthouse score of 100
7580859095100AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)10098989485
Lighthouse score of 100
7580859095100AstroTanStack StartNext 16 (App)Next 16 (Pages)Next 13 (Pages)10098989485
StageJSTotalHydratedFCP / LCPScore
Next 13 (Pages)271 KB1,592 KB3,430 ms1,724 ms85
Next 16 (Pages)169 KB440 KB2,898 ms1,952 ms94
Next 16 (App)165 KB394 KB2,829 ms1,580 ms98
TanStack Start120 KB278 KB1,765 ms1,148 ms98
Astro2.7 KB105 KB1,201 ms1,204 ms100
  • JS: kilobytes of gzipped JavaScript the page itself downloads by the time it’s interactive. Sandbox iframes not included.
  • Total: kilobytes of everything the page downloads by the time it has loaded and is interactive, gzipped (HTML, CSS, fonts, images, JS). Sandbox iframes not included.
  • Hydrated: milliseconds from navigation start to the site’s performance.mark('app:hydrated'), when scripts have run and the page is interactive. Astro has nothing to hydrate, so its mark is DOMContentLoaded, once its own scripts have run.
  • FCP / LCP: First Contentful Paint and Largest Contentful Paint in milliseconds. On these pages the largest paint is text, so the two are the same. First paint varies a lot load to load on the slow link, depending on whether the stylesheet or the preloaded fonts arrive first.
  • Score: Lighthouse’s performance score for its simulated mobile.

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

StageTime
Next 13 (Pages)457 ms
Next 16 (Pages)474 ms
Next 16 (App)337 ms
TanStack Start88 ms
Astro297 ms
  • Time: milliseconds from clicking a link on the home page until the post is on screen. The first frame after the client-side routers commit the route (the site’s app:navigated mark). The new document’s first contentful paint 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 (that doesn’t rely on implementation details) first.
  • Getting the agent to make the testing setup faster.
    • During development the agent ran the tests roughly 200 times. If not for the optimization they would have taken about 3 hours longer to run. So the one prompt of “the testing seems slow, can you make it faster?” paid off a lot.
    • The agent doesn’t have much of a sense of time, and will just put up with things being slow and not mention it.
    • It doesn’t hesitate to run a fresh build or test even when they’re slow, which is often more correct (a human might just keep coding without building or running tests), but exacerbates the issue.
  • 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.
    • 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. Done by hand these comparisons would likely be less thorough, and a full migration would potentially lead to succumbing to sunk-cost thinking.

What could have gone better

  • A couple visual regressions crept in. The agent was often good at reading visual diffs, identifying which part of the image changed and reasoning about it, but it missed some spacing bugs that it thought were noise.
    • I also missed these bugs because my git client doesn’t expand images to 100% zoom.
    • After this I had the agent build a local browser-based tool to review the diffs at 100% zoom.
  • 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 managed to reduce it from 10s to 2s.
    • The process was kind of like how people use AI to crack math problems by saying “you can do it, keep trying”.
    • Prompt: “3s is not very fast considering how few pages there are, try to make it faster”
    • This is similar to the migration in some ways. You need to have your outputs locked down and consider what changes you’re willing to take on for the optimizations. It got to a point where significant improvement would require complex incremental caching or forking Astro so I decided to stop there.
  • I dealt with some inaccurate metrics while working.
    • Lighthouse doesn’t measure first paint on a slow connection, it estimates it. The estimate assumes any script downloaded at high priority has to finish before the page can paint. Chrome downloads module scripts and modulepreload links at high priority, but they don’t block rendering, so Astro’s small prefetch script and TanStack’s preloaded bundles were counted against first paint when real testing showed no cost. Next loads scripts with async or defer, which download at low priority, so its measurements are not impacted.
    • The way I caught this was by noticing that Astro’s FCP was somewhat worse than Next 13’s, which I thought shouldn’t be the case since the HTML and CSS are almost identical and Astro is overall simpler. I investigated from there and made real measurements to confirm actual behaviour.
    • TanStack’s FCP measurement had been particularly inaccurate, indicating that its FCP was double Next 16’s.
    • Using a high priority for these modules is actually better since it leads to faster hydration without blocking FCP.
    • Benchmarking is easy to get wrong. Always assume the benchmark is wrong. Always look at the numbers and try to use your intuition to pick out any that don’t make sense with your understanding of the things being compared.

Conclusions

Developers know migrations or refactors should keep behaviour and appearance identical, and that experimenting should be done thoroughly and without sunk-cost bias. In practice we’re human, and often don’t have the time or stamina for it. With AI we can do things the way we know they should be done, more correctly, and with better results.

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. I did adjust the design after the migration, however I might have made more drastic changes if I hadn’t been building on top of existing content and code.


Dylan Vann
Dylan Vann
Software developer.

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