Client-Side Scalability

Astro and Svelte


Learning Objectives

  • You know how to add Svelte to an Astro project.
  • You know how to use Astro directives to control component hydration.

One of the features of Astro is that it is UI framework agnostic, meaning that it can work with a variety of UI libraries and frameworks. As we have previously worked with Svelte in the Web Software Development course, let’s add Svelte to our Astro project as well.

It would be possible to also add other frameworks like React, Vue, Alpine.js, and so on, and use them side by side in the same project. In this course, however, we use just Svelte, as we are already familiar with it.

If you are not familiar with Svelte, or feel that it’s been a while since you looked at it, you can always take a refresher and start from the Client-Side Development part of the Web Software Development course.

Adding Svelte support

First, in the folder client of the walking skeleton, run the commands deno install npm:svelte and deno install npm:@astrojs/svelte. This installs both Svelte and the Svelte integration for Astro.

deno install npm:svelte
deno install npm:@astrojs/svelte

Then, modify the astro.config.mjs file to include the Svelte integration. This involves importing the Svelte integration from the package loaded above, and adding it to a list of integrations.

import { defineConfig } from "astro/config";
import svelte from "@astrojs/svelte";
import deno from "@deno/astro-adapter";

export default defineConfig({
  adapter: deno(),
  integrations: [svelte()],
});

Finally, add a file called svelte.config.js to the client folder with the following content:

import { vitePreprocess } from "@astrojs/svelte";

export default {
  preprocess: vitePreprocess(),
};

This adds a preprocessor to the Svelte components, which is necessary for Astro to work with Svelte components.

First Svelte component

Now, let’s create a simple Svelte component. In the src/components folder, create a new file called FirstSvelteComponent.svelte with the following content:

<p>Hello Svelte!</p>

Then, modify the src/pages/index.astro file to import the Svelte component and to render it on the page. The modified index.astro file should look like this:

---
import FirstSvelteComponent from "./FirstSvelteComponent.svelte";
const greet = Astro.props.greet || "component";
---

<p>Hello {greet}!</p>
<FirstSvelteComponent />

At this point, if you have not yet restarted the walking skeleton, restart it. This way, the Svelte depedencies are downloaded also to the Docker container.

Now, when you open the page at http://localhost:8000, you should see a page similar to the one shown in Figure 1 below.

Fig. 1 -- The page at http://localhost:8000, among other things, shows "Hello Svelte!", which comes from a Svelte component.

Fig. 1 — The page at http://localhost:8000, among other things, shows “Hello Svelte!”, which comes from a Svelte component.

Next, let’s add some code to the Svelte component. Modify the component to match the following:

<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>Count {count}!</button>

The above showcases basic reactivity, state, and events in Svelte. In a Svelte application, when you click the button, the count increases by one.

However, when you click the button in a browser, the count does not increase. When we look at the HTML that the browser receives, we see that there is just the text <button>Count 0!</button> in the place of the Svelte component.

curl localhost:8000
// ...
<p
  data-astro-source-file="/app/src/pages/index.astro"
  data-astro-source-loc="7:6">Hello, Astro!
</p>
<p
  data-astro-source-file="/app/src/components/Hello.astro"
  data-astro-source-loc="6:4">
    Hello property and 8!
</p>
<!--[--><button>Count 0!</button><!--]-->
// ...

So.. Astro just turned the Svelte component into a static HTML element, dropping JavaScript. What’s going on?

Islands architecture and component hydration

Astro uses Islands Architecture for client-side rendering of UI framework components. Islands Architecture is a concept where sites are generated on the server with placeholders for the interactive components. When the sites are loaded to the client, the interactive components are hydrated with JavaScript.

By default, Astro renders all components statically on the server, dropping JavaScript from UI components during the process. This is done to minimize the amount of JavaScript sent to the client, which can improve the performance of the site. This is also why the Svelte component does not work as expected — the JavaScript that makes the button interactive is not included in the page.

Loading Exercise...

To control how UI components are handled, Astro provides a set of directives that can be used to control how the components are rendered and hydrated. These directives include client:load, client:idle, client:visible, and client:only, which are as follows.

  • The directive client:load instructs that the component should be loaded and hydrated immediately on page load.

  • The directive client:idle instructs to load and hydrate the component only when the browser has loaded the page and processed JavaScript on it. The component is considered low-priority — concretely, this is done by invoking the requestIdleCallback function.

  • The directive client:visible instructs to lazily load the component only when the user scrolls to the component.

  • The directive client:only instructs Astro to not render the component on the server at all. Instead, the component is loaded and hydrated on the client. As Astro’s documentation highlights, the client:only directive needs to be passed the framework as a value — in our case, we’d use this as client:only={"svelte"}. This provides Astro information that we’re using Svelte and that Svelte libraries should be sent to the client.

For example, to turn our Svelte component into an interactive component, but without haste, we can add the client:idle directive to the component in index.astro. The modified index.astro file should look like this:

---
import FirstSvelteComponent from "./FirstSvelteComponent.svelte";
const greet = Astro.props.greet || "component";
---

<p>Hello {greet}!</p>
<FirstSvelteComponent client:idle />

Now, when you reload the page, the button reacts to clicks, incrementing the count on each click. This is shown in Figure 2 below, where the user has clicked the button a handful of times.

Fig. 2 -- The button on the page has been hydrated and reacts to user interaction.

Fig. 2 — The button on the page has been hydrated and reacts to user interaction.

Using the directives, we can also control when the component is loaded. For example, if we add the client:visible directive to the component, the component is loaded only when it becomes visible on the page. This can be useful for optimizing the initial page load experience, especially when the component is not critical for the initial rendering of the page.

To try this out, add a handful of paragraphs before the FirstSvelteComponent in the index.astro file, and change the directive to client:visible. The modified index.astro file should look like this:

---
import FirstSvelteComponent from "./FirstSvelteComponent.svelte";
const greet = Astro.props.greet || "component";
---

<p>Hello {greet}!</p>
<p>plenty of lines</p>
<p>(copy and paste dozens of these before the Hello component)</p>
<p>plenty of lines</p>
<FirstSvelteComponent client:visible />

When you reload the page, you’ll see that the page loads as usual. If you open up the Network tab, you’ll see that the Svelte component is not loaded until you scroll down the page and reach the FirstSvelteComponent.

Loading Exercise...