Intro to Astro: Clever lazy loading for JavaScript

Essentially a build system, Astro takes a promising new approach to front-end JavaScript. And it works with React, Svelte, Vue, and other popular frameworks.

green galaxy 139404373
Thinkstock

Astro is a new approach to the current fervor in JavaScript: wringing more performance out of reactive front ends. It is developed by the same team that created the Snowpack build tool.

There have been several attempts to improve performance by avoiding the expensive prefetching and bootstrapping that have afflicted React-like frameworks. This is the notorious hydration problem described here.

Astro takes an interesting and novel approach. It is a build system that lets you use whatever framework you want (React, Svelte, Vue, etc.), and then does the work of finding where lazy loading can best be employed. You can think of this as a kind of smart code splitting applied to your app at bundle time.

So you get to use the same familiar framework you’re using now, but also gain potentially huge performance benefits.

Islands architecture

The web architecture Astro proposes to deliver is sometimes called islands architecture. The core idea is that the islands are your interactive, JavaScript-dependant components, surrounded by pure HTML/CSS markup.

By carving up the app in this manner, you can ship all of the HTML straight to the browser, so the user has something to interact with, while the JavaScript-dependent portions can be loaded only as needed. You can even tell Astro to defer the JavaScript until a component is visible to the user, as you’ll see below.

Working with Astro

Let’s start getting familiar with Astro by using the online sandbox. Click here to open it.

This URL will display a simple page, named Page.astro, with a time stamp. Note how the page (Listing 1) is broken into two sections. The first section, denoted by the first triple dash (---), contains the code that will be executed on the server at build time, not during run time. The second section, denoted by the second triple dash, contains the markup to be delivered at run time.

Listing 1. Simple Astro sandbox

---
import {format} from 'date-fns';

// Welcome to Astro!
// Write JavaScript & TypeScript here, in the "component script."
// This will run during the build, but never in the final output.
// Use these variables in the HTML template below.
//
// Full Syntax:
// https://docs.astro.build/core-concepts/astro-components/

const builtAt: Date = new Date();
const builtAtFormatted = format(builtAt, 'MMMM dd, yyyy -- H:mm:ss.SSS');
---
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Astro Playground</title>
    <style>
      header {
        display: flex;
        flex-direction: column;
        align-items: center;
        text-align: center;
        margin-top: 15vh;
        font-family: Arial;
      }
      .note {
        margin: 0;
        padding: 1rem;
        border-radius: 8px;
        background: #E4E5E6;
        border: 1px solid #BBB;
      }
    </style>
  </head>
  <body>
    <header>
      <img width="60" height="80" src="https://bestofjs.org/logos/astro.svg" alt="Astro logo">
      <h1>Hello, Astro!</h1>
      <p class="note">
        <strong>RENDERED AT:</strong><br/>
        {builtAtFormatted}
      </p>
    </header>
  </body>
</html>

Notice how the {builtAtFormatter} is used to reference the build-time variable within markup.

Add a component in Astro

Now let’s add a component. Click the plus icon in the file bar at the top as seen in Image 1.

Image 1. Add a component

astro play IDG

You’re new component will receive a default name (Component1.astro) and content, as seen in Listing 2.

Listing 2. Component1.astro

---
const name = "Component"
---

<h1>Hello {name}</h1>

Here again we have a simple variable assignment and display. Let’s make use of the component in the main page.

Return to Page.astro. Notice the system has helpfully inserted an import into the JavaScript segment:

 import Component from '@/Component.astro';

You can make use of this component by inserting <Component /> into the markup. Do that, and you will see the output of the child component in the preview window.

Using frameworks with Astro

Astro’s superpower is its support for a variety of other frameworks. It does this by employing their render engines during the build process, and compiling them into component “islands.” Let’s see how this works.

If you open this link you will see an Astro app running a Svelte component. (Here is an example demonstrating several render engines.)

The first thing to notice in the Svelte demo linked above is the astro.config.mjs file. This contents of this file will look something like Listing 3.

Listing 3. Enable the Svelte renderer

export default /** @type {import('astro').AstroUserConfig} */ ({
  // Enable the Svelte renderer to support Svelte components.
  renderers: ['@astrojs/renderer-svelte'],
});

Listing 3 shows you how to enable Svelte, so the engine will understand Svelte components. We can now import a Svelte file right into the Astro file. For example, let’s add this line to /pages/index.astro:

import Counter from '../components/Counter.svelte

Now we can then use the Counter from Svelte in Astro as shown in Listing 4.

Listing 4. Using a Svelte component in Astro

<Counter client:visible>
   <h1>Hello, Svelte!</h1>
</Counter>

Although this is typical Svelte usage, note there is an Astro-specific property on the Counter: client:visible. This means the component will not be loaded into the client unless it is visible on the page. Thus it achieves some granular lazy loading with a minimum of effort.

At the time of writing, Astro supports Svelte, React, Vue, Solid, Preact, and Lit. The process for using them is just like with Svelte. In fact, you can enable multiple render engines and use them side by side in your Astro app.

In addition to integrations, Astro also makes several themes and starters available.

Fine-tuning partial hydration with Astro

You’ve seen the client:visible directive in action. There are others available. In each case, the directive first tells Astro to render the component on the client with its attendant JavaScript, instead of doing a server render and sending the HTML. Then it tells Astro how to go about hydrating the component.

Astro client directives

Astro’s client directives control how components are hydrated on the page.

  • <MyComponent client:load /> : Hydrates the component on page load.
  • <MyComponent client:idle /> : Hydrates the component as soon as the main thread is free (uses requestIdleCallback()).
  • <MyComponent client:visible /> : Hydrates the component as soon as the element enters the viewport (uses IntersectionObserver). Useful for content lower down on the page.
  • <MyComponent client:media={QUERY} /> : Hydrates the component as soon as the browser matches the given media query (uses matchMedia). Useful for sidebar toggles, or other elements that should only display on mobile or desktop devices.
  • <MyComponent client:only={string} /> : Hydrates the component on page load, rendering only on the client. Takes the framework of the component as a string (e.g., "svelte").

The build-time approach

Because Astro is fundamentally a build tool, it has complete control over what is ultimately shipped to the user’s browser. That means in addition to doing clever things with lazy-loaded JavaScript, Astro can be smart about how it delivers other assets like CSS.

Moreover, the aim of Astro is to distill as much JavaScript as possible down to straight HTML, meaning less data over the wire, less browser churn, and faster time to interactive.

Overall, although Astro is admittedly more geared towards static sites, it’s a promising and innovative approach—and a very active project, with nearly 16 thousand stars on GitHub.

Copyright © 2022 IDG Communications, Inc.