A Rebuild in Astro

I wouldn't be a web dev if I didn't rebuild my site annually

I really like Next.js. It's an excellent framework to build complex sites and applications, and it's solved a lot of the problems you're likely to run into when working with React.

That said, my site isn't really that complex. I've got a few interactive bits, but it's mostly static. I didn't like the fact that I was serving 100kb of JS for what was essentially static text.

Looking at the options

So I started looking at other options. I knew I wanted something that let me work in Typescript, and I knew I wanted the ability to use server-side endpoints for areas of my site that need to update more frequently. I checked out eleventy, which I've seen a few people using (and used for a very simple holding page), but after using Next.js with MDX and React components I just couldn't go back to other templating languages like nunjucks or handlebars.

Then I remembered another framework I'd heard about, that lets you generate mostly static pages, but with dynamic islands of content injected where needed. After some digging throug my bookmarks I took a look at Astro, and it sounded pretty good so I decided to try building a few of my existing pages to see how it was to work with.

Astro components

Astro uses a syntax very similar to React for it's components (but just different enough to repeatedly trip me up). I'm not going to go into all the details, but each component is split into 2 parts, a script (written in JS) and a template (HTML, but with some JS for template logic). The difference to React is that anything run in the script section won't be run on the client, and anything in the template section is entirely static which makes for a nice clear separation.

Little sprinkles of JS

If you do need some client side interactivity then Astro lets you add a <script> tag right in the same component, and handles bundling them all up for you. I'll use my Spotify embed component as an example, because I want to show it off.

This component takes a prop trackId="1FTSo4v6BOZH9QxKc3MbVM", and then at build time it calls the Spotify API to grab all the relevant information. On the client side there's some simple JS to handle turning artwork into a simple audio player for the preview, but if JS is disabled the track itself renders just fine. There's some additional JS that is only included if I set the track ID to current, which will call an API endpoint on the client to get my current (or most recent) track

Big dollops of JS

Sometimes you want to build something that would be an absolute pain in vanilla JS, and that's where the "dynamic islands" come in. It really is the best of both worlds. lean static HTML for the vast majority of the site, small snippets of JS for some progressive enhancement, but I can still use a framework if I want to throw together something more dynamic.

Astro can handle a whole range of UI frameworks, I'm using preact for my (currently empty) pokedex and my dumb carrot game.

The results

The results when looking at total download sizes speak for themselves. I've set up a speedlify instance to track the page speed results for some pages. I really wish I'd had this set up before I switched over to the new site, just to see the graph drop.

PageNew sizeOld size
Homepage17 KiB187 KiB
Annual Review 202130 KiB178 KiB
Pokedex98 KiB254 KiB
Carrot Game31 KiB158 KiB
Diablo Character16 KiB332 KiB
Article List20 KiB160 KiB
My Tailwind Article26 KiB160 KiB

The actual page speed scores though aren't much different. Here's a comparison of the lighthouse scores for my 2021 annual review:

SitePerformanceAccessibilityBest PractisesSEO
New Astro Site99100100100
Old Next.js Site989710090

Overall sizes that are 5-10x smaller result in an an increase of the performance score from 98 to 99. I think this says more about the amount of work Next.js does behind the scenes to optimise the rendering experience (and probably how well Vercel works with Next.js in general).

But at least I can sleep well at night knowing that even my largest page is less than 100K.