Skip to content

Building Web Applications using Astro - What makes it special?

You might already have heard that there is a new player in the static site generator space that is generating a lot of excitement and asking some hard questions of the modern Javascript ecosystem. I'm talking about Astro, a framework constructed around two simple but revolutionary concepts:

  1. Accepting components from any UI framework
  2. Partial hydration

I have recently been working on a site built from the ground up using Astro, and even in the early state its in, I've been able to see the amazing possibilities it opens up in web development. So, let me give you a tour of what those two points mean, both conceptually and for the future of Javascript development.

Bring your own framework

The modern Javascript ecosystem is divided into separate and sometimes very distant camps, based on what UI library you're using. Next for React, Nuxt for Vue, SvelteKit for Svelte, etc. It doesn't have to be this way. All of these UI libraries ultimately use the same Javascript to output and transform the same HTML and CSS. Why should you have to replace your whole toolkit just to use a different brand of paint?

Astro uses a set of plugin renderers to support components written in different formats. Currently, they have ones for React, Preact, Vue, and Svelte, as well as supporting their own minimalist templating format in .astro files. However, there's no reason why this couldn't be expanded by the community in the future.

Of course, to hydrate components on the client you need to bring the framework's runtime with you, so it's never going to be particularly efficient to mix and match components from different frameworks on the same page. But, if you absolutely have to, it's now an option. What's much more important is the ability to leverage the potential of Astro, no matter which set of tools you're most familiar with. In a future where more parts of the ecosystem plug into different libraries like this, we might even see more iteration in the UI library space without new entrants having to worry about producing an end-to-end build, serve and hydrate story.

Just a sip (of client hydration)

The framework compatibility is great, but at the end of the day, the only thing it meant for us was that using Astro with React was possibile. It's all well and good that you can use it, but why would you want to? Partial hydration is the answer.

You see, most websites you will ever build have some interactivity beyond just clicking hyperlinks. It isn't the 90s anymore, rich app-like behavior is increasingly part of user expectations. This is why frameworks like Vue and React have taken off the way they have. However, most websites you will ever build also have large sections which don't have any interactivity beyond clicking hyperlinks. Maybe you have a blog that is totally static except for a search bar and a comment section. Or a marketing site that includes a carousel and a navigation popover. Or even a very complex interactive app, which is mostly fully interactive, but needs some highly-performant static marketing and e-commerce pages to sell it to customers.

This is the "islands of interactivity" model. Most web applications are a mix of static content and interactive widgets. However, in all of our frameworks, even if they pre-generate some HTML at build time or on the server using Server-Side Rendering, if you want to use components, you have to download those components on the client. Even if they're never going to do anything! A set of paragraphs with CSS classes that won't ever change? The client has to download that twice: once in the HTML that will actually be displayed, and then again as a sluggishly-parsed Javascript bundle that will execute during hydration, but have exactly no effect on the result.

Astro says "no" to this inefficient use of resources, and gives us a rich set of tools to send to the client only the Javascript that is minimally necessary to enable interactivity. This is called "partial hydration" because only the islands of interactivity, those widgets that you mark as needing to change, get "hydrated" by loading the client-side version of their components.

Astro components

The way this is all done is with a new syntax for HTML templating that allows you to include components from your framework of choice with annotations that decide whether they should hydrate, as well as Javascript, to execute during build-time. The following is an astro component extracted from a project I am currently working on, and only lightly edited, showing most major features:

---
import '$system/src/globals/global-styles'
import { fontHeadTags } from '$system/src/globals/font-import.mjs'
import { sprinkles } from '$system/src/sprinkles/sprinkles.css'
import { reactTheme } from '$system/src/themes/themes.css'
import { Sidebar } from '$system/src/components/sidebar.tsx'
import { MobileNav } from '../components/mobile-nav.tsx'
import { fetchCategories } from '../models/category.ts'

const { title } = Astro.props

const categories = await fetchCategories()
---
<!DOCTYPE html>
<html class={reactTheme} lang="en">
  <head>
    {fontHeadTags}
    <title>react.framework.dev | {title}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8"/>
  </head>
  <body>
    <MobileNav client:media="(max-width:1024px)" categories={categories} />
    <Sidebar>
      <div class={sprinkles({ layout: "stack", gap: 24 })}>
        {categories.map(category => (
          <a href={`/categories/${category.slug}`}>
            {category.name}
          </a>
        ))}
      </div>
    </Sidebar>
    <main class={sprinkles({ marginX: 64, marginY: 48 })}>
      <slot />
    </main>
  </body>
</html>

The section between the --- is "frontmatter" (a concept borrowed from Markdown-based site generators), but as opposed to Markdown frontmatter, it is not restricted to just declaring data. Any JavaScript code can be imported or run in frontmatter, and it will be executed whenever the template is rendered during build time. This makes it very convenient and powerful for fetching and processing data.

The rest of the file is a JSX template, which hopefully looks fairly familiar. This isn't full React — no reactive state or event handlers — just a way to produce static HTML from a syntax that requires less specialized syntax knowledge than something like Handlebars because you can just use .map for loops and && for conditional rendering. You can include any HTML tag, any component in the framework of your choice, and one or more <slot /> tags, which will be replaced with the inner HTML — or "children" in React parlance —that the component is rendered with. The code above is for a main layout component, so our <slot /> will contain the page content.

The final special of feature of Astro components is client: directives. You can see an example on the MobileNav component above. Client directives can be placed on any component authored in a framework that has a client runtime (so, currently, Vue, React or Svelte) and when the conditions met by the directive are met, Astro will load the framework runtime and that component's code, and hydrate it to make it interactive. There are a number of directives for different conditions:

  • client:load hydrates on page load. This mirrors how SPAs are usually loaded.
  • client:idle hydrates as soon as the main thread is free. This should theoretically get you interactivity as soon as possible at the cost of potentially delaying loading, but I haven't experimented with it.
  • client:media hydrates when the browser matches a media query. This is great for content that will only be shown to certain devices, like our mobile nav!
  • client:visible hydrates when the component becomes visible. This is great for content that might be below the fold. You can load the page as fast as possible, and only download Javascript if the user scrolls.
  • client:only doesn't output anything at build time and instead of hydrating does from-scratch client rendering on load. It's almost always better to use client:load and provide a placeholder, but there can be cases where a component simply cannot be made to run outside a browser.

The result of the above is that on desktop we have a fully-functional site with no Javascript at all — Sidebar is a React component, but it just renders a static menu with <a> tags for navigation — and on mobile, we download React and only what is needed to make the MobileNav component work. This allows us to still use Javascript and Javascript UI frameworks, but our users only pay the price in performance for the features that they actually see, without an extra tax just to allow us to have a unified developer experience!

I hope you now understand why Astro is exciting, and why we jumped to try it out even though it's still in its early days. If you want to learn more about it, check out their documentation and their extensive repository of examples.