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.
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.
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, theclient:only
directive needs to be passed the framework as a value — in our case, we’d use this asclient: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.
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
.