iJS CONFERENCE Blog

Island Architecture with Astro

Astro extends the capabilities of established web technologies

Jan 17, 2023

Astro provides Progressive Hydration for creating performant content-driven web solutions. It combines pre-rendered page areas with interactive islands that the browser loads only when it needs them.

Performance is one of the most important architectural criteria, especially for public web solutions. Unfortunately, the flood of JavaScript bundles referencing modern web solutions throws a spanner in the works. Working without JavaScript isn’t an option. Therefore, it’s important to pre-render as much as possible on the server and load JavaScript bundles as late as we can. The parts that the user doesn’t interact with don’t even need to be loaded at all. The same applies to code that is initially used for just server-side rendering.

Astro [1] is a new framework that implements these strategies for content-driven websites. A good example of this are product pages with a lot of content and some interactive areas. What’s special about Astro is that the interactive areas (also called islands) can be written with established frameworks like Angular, React, or Vue.

Additionally, Astro provides for two modes of operation. It generates a static website by default. For this, it uses data retrieved via HTTP and islands. For more flexible scenarios, Astro can also be instructed to render the requested pages on the server side for each individual request. In this article, I will present Astro using a simple application. The examples used can be found at [2].

Client-side rendering in SPAs

Single Page Applications move all rendering to the client side, i.e. to the browser. This improves response times and increases the user experience enormously. However, because an SPA needs to load JavaScript bundles and data at startup, client-side rendering also delays the program launch. As Figure 1 shows, this means that quite a bit of time elapses before the First Meaningful Paint (FMP).

 

 

Fig. 1: Client-side rendering

 

An initial delay of a few seconds may not matter for business applications. There, it’s more a matter of the application behaving smoothly after startup. But in public portals, where conversion is the primary concern, it’s a different story. Every millisecond counts when keeping bounce rates low.

Search engines can still handle HTML better than JavaScript. That’s why portals based on SPA frameworks usually use server-side rendering (SSR). Figure 2 illustrates the difference with client-side rendering of SPAs.

 

Fig. 2: Server-side rendering

Now, the delivered HTML already contains all the information that makes up the page in question. The First Meaningful Paint (FMP) happens earlier. However, to make the page interactive, it still has to load individual JavaScript bundles. They bring the static HTML to life. This is also called hydration. So that the JavaScript bundles don’t have to reload data that was previously used in the server-side rendering, the server tucks it away in an invisible area of the page, such as in a script tag.

Although the FMP happens earlier with SSR, it takes longer to get to Time to Interactive (TTI), especially since the page has to wait for the bundles to load. Another reason for this is that the initial HTML is now larger. There’s a period of time between FMP and TTI where the application looks interactive, but it isn’t. This is called the Uncanny Valley.

iJS Newsletter

Keep up with JavaScript’s latest news!

Partial and Progressive Hydration as a solution

In 2016, the well-received article “When everything’s important, nothing is!” [3] already highlighted the problem discussed in the last section, and proposed “Progressive Booting” as a solution. However, with the frameworks of the time, implementing this method wasn’t readily possible. Today, it’s better known as Progressive Hydration.The idea behind this is that the page hydrates individual areas gradually and progressively. Therefore, the currently visible page area can quickly become interactive (Fig. 3).

Fig. 3: Progressive Hydration

Some parts of the page may not need to be hydrated at all. These may be parts that the user doesn’t use, or parts that are static by design. Examples include navigation bars, footers, or content areas in articles. If you can identify these areas, the hydration effort is reduced even further. This is called partial hydration (Fig. 4).

Progressive Hydration with Island Architecture

Astro offers support for progressive and partial hydration with its Island Architecture. The idea behind this is a page has interactive islands embedded in static content areas. Only the latter need to be hydrated. In Figure 5, only the box at the bottom is an interactive island.

Fig. 5: Island Architecture in Astro

Depending on the settings you choose, Astro won’t hydrate individual islands until they are scrolled into the visible area, for instance. The interesting thing is that these islands can be written using almost any current JavaScript technology. Integrations for the big three (Angular, React and Vue) are available, among others (Fig. 6).

Fig. 6: Islands based on different frameworks

Astro doesn’t make existing JavaScript technologies obsolete. Rather, it extends their area of application as a metaframework. Developers can build upon their existing knowledge and use existing code.

Anatomy of an Astro solution

An Astro application consists of three types of building blocks: layout, pages, and components (Figure 7).

Fig. 7: Structure of an Astro application

Pages get their own URL and can also accept parameters. To ensure that a solution’s pages look uniform, Astro embeds them in layouts. A layout defines the “surroundings” and its basic structure. For example, it defines the header and footer areas, navigation, and general styles. Individual pages can be subdivided into different components.

The components include Astro’s own components, which it renders on the server side and don’t require JavaScript on the client side. Islands that rely on an integrated framework such as Angular, React, or Vue also count as components. Unlike Layouts, Pages, and Astro components, Astro renders the islands it uses on the server-side and also hydrates them client-side. So they come to life in the browser, on demand.

Astro also provides separate folders for the three types of Building Blocks. Individual files must end with the suffix astro. These files combine program code, markup, and styles. The code itself is TypeScript. To simplify working with astro files, there are plug-ins for various IDEs, such as Visual Studio Code.

EVERYTHING AROUND ANGULAR

The iJS Angular track

Layouts

To illustrate the interaction between individual Building Blocks, let’s take a look at the demo application’s source code provided at [1]. The individual pages share a layout that provides the solution’s basic framework (Listing 1).

(Listing 1).

---
// layouts/layout.astro
 
export interface Props {
  title: string;
}
 
const { title } = Astro.props;
---

<!DOCTYPE html>

<html lang="en">

  <head>

    [...]

    <title>{title}</title>

  </head>

  <body>


    <!-- Your header could be here -->

    <!-- Your navigation bar could be here -->

    <slot />

    <!-- Your footer could be here -->

    <style>

    /* Your central styles could be here */

    </style>

  </body>

</html>







 

 

Since the layout is used for multiple pages, it accepts the respective page title via its title property. The Props interface defines the supported properties. The name Props is a convention. If an interface with this name exists, Astro picks it up without any further action. The slot element defines the position in the markup where the respective page will be placed.

Pages

A page is subdivided into a layout and delegates to components. Both the layout and components are included via ECMAScript imports that reference astro files. Astro derives elements from them that can be used in the markup (Listing 2).

Listing 2: Simple Astro Page

---

// pages/index.astro

import Layout from '../layouts/Layout.astro';

import FlightCard from '../components/FlightCard.astro';

import type { Flight } from '../model/Flight';

import { BASE_URL } from '../conf';

const flights = await fetch(`${BASE_URL}/assets/data.json`)

                .then(r => r.json()) as Flight[];

---

<Layout title="Welcome to Astro.">

  <main>

    <h1>Last Minute <span class="text-gradient">Astro</span></h1>

    <p class="instructions">

      Lorem, ipsum dolor sit amet consectetur adipisicing elit. 

    </p>

    <ul role="list" class="link-card-grid">

      {flights.map(f => <FlightCard flight={f}></FlightCard>)}

   </ul>

  </main>

</Layout>

 

The example shown first retrieves a few flight objects via HTTP. It does this using the browser native fetch function. Then it calls the layout and passes a value for its title property. It maps the retrieved flights to other HTML elements using map. These elements trigger the Astro component FlightCard, which receives the respective flight via its flight property (Listing 3).

Listing 3: Astro component

---

// components/FlightCard.astro

import type { Flight } from '../model/Flight';

export interface Props {

  flight: Flight

}

const { flight } = Astro.props;

---

<li class="link-card">

  <a href={`detail/${flight.id}`}>

    <img src={flight.img} width="100%">

    <h2>

      {flight.title}

      <span>&rarr;</span>

    </h2>

    <p>

      Lorem ipsum dolor amet sammas ergo gemma.

    </p>

  </a>

</li>

 

Like the layout, the Astro component shown here defines its properties with an interface called Prop. It represents the flight obtained as a property with data binding expressions in the markup. Since Astro components are only rendered on the server side, but not hydrated, no data bindings exist for events or matching with form fields. These use cases are handled by interactive islands.

File-based routing

Instead of defining routing rules programmatically, Astro derives them using conventions from the folder structure and file names. As the example in Figure 7 shows, Astro causes the route detail/7 to be associated with the file [id].astro. It gives the 7 as the id parameter.

If Astro is used to pre-render a static website, the page must map every possible route parameter to properties. By convention, this requires setting up an asynchronous getStaticPaths function that returns an array of parameter/property pairs (Listing 4).

Listing 4: Astro page with routes and prerendering

---

[…]

export async function getStaticPaths() {

  const flights = await fetch(`${BASE_URL}/assets/data.json`)

                  .then(r => r.json()) as Flight[];

  return flights.map((flight) => {

    return {

      params: { id: '' + flight.id },

      props: { flight }

    };

  });

}

const {id} = Astro.params;

const {flight} = Astro.props;

---

<Layout title="Welcome to Astro.">

  <main>

    <h1>{flight.title}</h1>

    <p class="instructions">

      Lorem, ipsum dolor sit amet hammas oda. 

    </p> 

    <img src={flight.img}>

    <p>

      Lorem, ipsum dolor sit gemma ham ...

    </p> 

    […]

  </main>

</Layout>

 

For each parameter/property pair Astro supplies, it generates a page in the build. If prerendering isn’t flexible enough for the desired use case, Astro’s configuration can be set to SSR mode [4]. In this case, it renders the page on every single call to the server. For this, Astro comes with integrations for various hosting environments. Besides general integrations in Node.js and Deno, there are also specific ones for hosts like Vercel, Cloudflare, or Netlify.

The getStaticPaths function is no longer used in SSR mode. Instead, the page can access the given parameters directly via the Astro object and load data per page load. The page also has access to HTTP headers in SSR mode, and can read cookies or initiate redirects, for instance.

Integrating islands

Interactive islands are components based on one of the included frameworks and can be found in the components folder. They can be included similarly to Astro components with ECMAScript imports (Listing 5). Even variables that Astro uses in the context of prerendering or SSR can be bound to properties of these components. The values are available even after hydration. Additionally, client directives such as client:visible can be used to control when Astro should trigger the component’s hydration (Table 1).

Listing 5: Calling interactive islands

---

[…]

import PriceReact from '../../components/Price';

import PriceNg from '../../components/PriceNg'

import PriceVue from '../../components/Price.vue';

[…]

---

[…]

<PriceReact client:visible title={flight.title}></PriceReact>

 

<PriceVue client:visible title={flight.title}></PriceVue>

 

<PriceNg client:visible title={flight.title}></PriceNg>

[…]

 

Table 1: Client directives determine when a component is hydrogenated

Conclusion

As a metaframework, Astro extends the capabilities of established web technologies. This makes sense because these technologies are widely used and teams can build upon their existing knowledge.

While other frameworks pursuing modern hydration scenarios painstakingly try to figure out which stretches of code the client actually needs, Astro makes things a bit easier. It only hydrates islands. It simply pre-renders everything else — either at build time or at runtime on the server side. Astro focuses clearly on the simple development of performant, content-driven web solutions like product pages. But for other types of web solutions, Astro is not necessarily a good choice.

Links & Literature

[1] https://astro.build/

[2] https://github.com/manfredsteyer/astro-last-minute

[3] https://aerotwist.com/blog/when-everything-is-important-nothing-is/

[4] https://docs.astro.build/en/guides/server-side-rendering/

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows