đ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.
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
: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.)
â prerendering.
astro-2.0 adds support for
If youâre interested, you can take a look at the code on GitHub.