🖋Migrating from Next.js to Astro

–

I had a long weekend due to Christmas and some time to spare so I decided to migrate my website from Next.js to Astro. The result is a quite impressive Lighthouse score.

A perfect Lighthouse score showing 100 for Performance, Accessibility, Best Practices, and SEO

Next.js setup

My previous setup used Next.js with SSG (Static Site Generation) to generate HTML files for all pages, which were statically served by AWS Cloudfront from an S3 bucket. I also use a similar setup (SSG + static hosting) on a couple of work projects, and it works reasonably well: page load is fast and hosting is cheap.

One issue with Next.js is that it still bundles the React to hydrate the pages on the frontend (i.e., to “attach” React to server-rendered HTML, so it becomes interactive). That is a pure waste since my posts are static and there is no interactivity.

Another caveat is that static hosting with Next.js is a second-class citizen and some features don’t work. Most notably, Next.js’ built-in image optimization does not work, and you have to jump through hoops writing your own image optimizer to fix it (which is no fun, believe me).

Astro

Astro is a web framework for building fast, content-focused websites (my website qualifies as well as most blogs, landing pages, and documentation websites). Unlike Next.js, Astro focuses on SSG and static hosting—all features work in SSG, including image optimization via @astrojs/image.

Thanks to explicit focus on content-rich websites, Astro makes trade-offs that wouldn’t fly for a regular web app. For example, Astro does not hydrate components by default—the result is a pure HTML page with no client-side JavaScript whatsoever.

Astro is not a pure static website generator though (like Jekyll, Hugo, or Eleventy). If you need interactivity, Astro allows including React, Vue, Svelte, or Solid.js components (and even mixing them to some degree). By default, components are server-only (Astro runs them on server to generate HTML but they are not hydrated on the client side, so no client-side JS is shipped). With one client:load directive, Astro converts them to “Islands”—interactive portions of a webpage that are hydrated and managed by UI frameworks.

---
import MyDynamicReactComponent from './Dynamic.jsx';
import MyStaticReactComponent from './Static.jsx';
---
<div>
  <MyStaticReactComponent />
  <MyDynamicReactComponent client:load />
</div>

Because Astro Islands are independent sections of a webpage, they can be initialized (hydrated) independently. client:load hydrates them as soon as the page is loaded, but there are other strategies as well. client:idle hydrates components in requestIdleCallback, and client:visible hydrates the component when it’s scrolled into view.

The latter directive is extremely powerful. On my website, I have a small React-powered subscription form in the footer. Because the footer is at the bottom of the page and is not immediately visible, by adding a single directive I was able to exclude React runtime from the first-load JS bundle and save ~110kB.

<main>
  <Header title={post.title} />
  {/* Static post */}
  <Post post={post} />
</main>
<footer>
  <SubscriptionForm client:visible />
</footer>

Overall, by migrating to Astro, my JS bundle decreased from ~200kB to 4kB, and home page loads at ~100kB.

Pushing to 100s: fonts

To get 100 score for Performance, the last thing that I did is self-hosting webfonts and preloading them.

This is as simple as importing them from @fontsource and adding a <link> in a head:

---
import '@fontsource/libre-baskerville/400.css';
import font400 from '@fontsource/libre-baskerville/files/libre-baskerville-latin-400-normal.woff2';
---
<link
  rel="preload"
  href={font400}
  as="font"
  type="font/woff2"
  crossorigin="anonymous"
/>

Extra features: RSS and Firebase Hosting

While I was at hacking my website, it was pretty easy to add RSS feeds with @astrojs/rss. Now you can use posts.rss.xml to get all my 🖋 posts, or archive.xml.rss to get everything.

I also migrated my deployment to Firebase Hosting because GCP seems to be greener alternative to AWS but also because Firebase is very easy to use. Deploying Astro to Firebase Hosting was a breeze.

Summary

I enjoyed the migration process and I’m impressed by the result. If you have a content-heavy website, I highly recommend taking a look at Astro—especially if you’re using Next.js—Astro has everything that you need but generates faster and smaller pages.

Pros:

  • No client-side code is generated by default. This means smaller bundle size and faster page load.

  • You can author fully-static pages using either Astro markup language or any familiar framework (React, preact, Vue, Svelte, Solid.js). Astro will use your React component to generate static markup during build time. If you have a mostly-static website, it is quite easy to convert as you can reuse existing components.

  • Unlike fully-static site generators, Astro allows creating interactive sections with Astro Islands. Each Island is loaded independently and may load at different times or use different UI frameworks.

  • Astro has a turn-key solution for image optimization (as well as many other useful integrations). And every feature works great with SSG.

A couple of caveats as of [2022-12-26 Mon]:

  • Astro uses Vite for the build and I experienced some minor issues with some CJS packages in either dev or prod modes (e.g., I couldn’t get react-use to work). Most of these issues are resolved by upgrading packages.

  • Astro has SSR support (running a webserver to generate pages on-the-fly) but it seems to completely disable static generation. So you can pick either full SSG or full SSR. (For the contrast, Next.js is able to serve pages selectively via either pre-generated pages or on-the-fly generation.)

If you’re interested, you can take a look at the code on GitHub.