Using Astro on framework.dev
Have you heard of Astro? It's an exciting, if still experimental, static site generation framework that allows you to author components using your choice of framework and completely control if and when javascript is shipped to the client. If you would like to learn more, I wrote an introductory blog post about it.
Considering how new and experimental Astro is, you might be wondering what it's like to actually try to build a website with it. Well, you're in luck because we chose to build react.framework.dev in Astro, and I'm here to tell you how it went.
Some background
When I was first presented the pitch for the framework.dev project, it had the following characteristics:
1. It was going to be primarily a reference site, allowing users to browse static data.
2. It should be low-cost, fast and accessible.
3. Because it was a small internal project, we were pretty free to go out on a limb on our technology choices, especially if it let us learn something new.
At the time, I had recently heard exciting things about this new static site generator called "Astro" from a few developers on Twitter, and from cursory research, it seemed perfect for what we had planned. Most of the site was going to be category pages for browsing, with search existing as a bonus feature for advanced users. Astro would allow us to create the whole site in React but hydrate it only on the search pages, giving us the best of the static and dynamic worlds. This plan didn't quite survive contact with the enemy (see the "ugly" section below) but it was good enough to get the green light.
We also picked vanilla-extract as a styling solution because we wanted to be able to use the same styles in both React and Astro and needed said styles to be easily themeable for the different site variants. With its wide array of plugins for different bundlers, it seemed like an extremely safe solution. Being able to leverage Typescript type-checking and intellisense to make sure theme variables were always referenced correctly was
certainly helpful. But as you'll see, the journey was much less smooth than expected.
The Good
Astro did exactly what it had promised. Defining what static pages would be generated based on data, and what sections of those pages would be hydrated on the client, was extremely straightforward.
The experience was very similar to using NextJS, with the same system of file system paths defining the routing structure of the site, augmented with a getStaticPaths function to define the generation of dynamic routes. However, Astro is easier to use in many ways due to its more focused feature set:
- Very streamlined and focused documentation.
- No potential confusion between static generation and server-rendering.
- All code in Astro frontmatter is run at build-time only, so it's much easier
to know where it's safe to write or import code that shouldn't be leaked to
the client bundle.
- No need to use special components for things like links, which greatly simplifies writing and testing components. Our Storybook setup didn't have to know about Astro at all, and in fact is running exactly the same code but bundling it with its own webpack-based builder.
Choosing what code should be executed client-side is so easy that describing it
is somewhat anticlimactic:
`
However, this is a feature that is simply not present in any other framework, and it gives a site very predictable performance characteristics: each page loads only as much Javascript as has been marked as needing to be loaded. This means that each page can be profiled and optimized in isolation, and increases in complexity in other pages will not affect it. For example, we have kept the site's homepage entirely static, so even when it shares code with dynamic areas like search, the volume of that code doesn't impact load times.
The Bad
Although we were pleasantly surprised by how many things worked flawlessly despite Astro being relatively new, we were still plagued by a number of small issues:
- A lot of things didn't work in a monorepo. We had to revert to installing all npm libraries in the root package rather than using the full power of workspaces, as we had trouble with hoisting and path resolution. Furthermore, we had to create local shims for any component we wanted to be a hydration root, as Astro didn't handle monorepo imports being roots.
- We were hit multiple times with a hard-to-trace issue that prevented hydration from working correctly with a variety of errors mostly relating to node modules being left in the client bundle. The only way to fix this was to clear the Snowpack cache and build the site for production before trying to start the dev server. You can imagine how long it took to figure out this slightly bizarre workaround.
- Astro integration with Typescript and Prettier was pretty shaky, so the experience of editing Astro components was a bit of a throwback to the days before mature Javascript tooling and editor integrations. I'm very thankful that we had always intended to write almost all of our components in React rather than Astro's native format.
We also hit a larger hurdle that contributed to the above problems' remaining issues for the lifetime of the project: Astro moved from Snowpack to Vite in version 0.21, but despite vanilla-extract having a Vite plugin, we were unable to get CSS working with the new compiler. It's uncertain whether this is an issue with Astro's Vite compiler, or whether it's down to vanilla-extract's Vite plugin not having been updated for compatibility with Vite's new (and still
experimental) SSR mode that Astro uses under the hood. Whatever the reason, what
we had thought was a very flexible styling solution left us locked in version
0.20 with all its issues. The lesson to be learnt here is probably that when dealing with new and untested frameworks it's wise to stick to the approaches recommended by their authors, because there's no guarantees of backwards compatibility for third-party extensions.
The Ugly
As alluded to in the introduction, our plans for framework.dev evolved in ways that made the benefits of Astro less clear. As the proposed design for the site evolved, the search experience was foregrounded and eventually became the core of every page other than the homepage. Instead of a static catalogue with an option to search, we found ourselves building a search experience where browsing was just a selection of pre-populated search terms.
This means that, in almost all pages, almost all of the page is hydrated, because it is an update-results-as-you-type search box. In these conditions, where most pages are fully interactive and share almost all of their code, it's arguable that a client-side-rendering SPA like you'd get with NextJS or Gatsby would be of equal or even superior performance. Only slightly more code would have to be downloaded for the first view and subsequent navigation would actually require less markup to be fetched from the server.
The flip side of choosing the right tool for the job is that the job can often change during development as product ideas are refined, and feedback is incorporated. However, even though replacing Astro with NextJS would be fairly simple since the bulk of the code is in plain React components, we decided against it. Even Astro's worst case is still quite acceptable, and maybe in the future, we will add features to the site which will be able to truly take advantage of the islands-of-interactivity hydration model.
Closing thoughts
Astro's documentation does not lie. It is a very flexible and easy to use framework with unique options to improve performance that is also still firmly in an experimental stage of its development. You should not use it in large production projects yet. The ground is likely to shift under you as it did with
us in the Snowpack-to-Vite move.
The team behind Astro is now targeting a 1.0 release which will hopefully mean greater guarantees of backwards compatibility and a lack of major bugs. It will be interesting to see what features end up making it in, and whether developer tools like auto-formatting, linting and type-checking are going to also be finished and supported going forward. Without those quality of life improvements, Astro has still been an exciting tool to test out, which has made me think differently about the possibilities of static generation. With them, it might become the only SSG framework you will need....