Vue.js + Astro - Better Than A Vue SPA?
Anthony Gore | October 26th, 2021 | 5 min read
A lot of devs have announced recently that they've migrated their site to Astro. This is usually accompanied by a screenshot of a near-perfect Lighthouse score and a series of rocket emojis.
Like most people, I find the endless parade of new frameworks tiring. But I've done some playing around with Astro and IMO it's really worth checking out.
In this article, I'll show you how you can build a Vue-based app using Astro and we'll see how its unique architecture can lead to potentially better performance than a single-page app (SPA).
Table of contents:
Recap of the SPA architecture
Before we see Astro in action, we'll need to get an understanding of it's architecture. To do this, let's first remind ourselves of the pros and cons of the single-page app architecture.
SPAs abstract all the functionality and content of a site into JavaScript components. This is great because it makes development of the site easy.
The downside of this approach comes when the site is put into production. All these JavaScript components get bundled together into one big app. Due to the size, the app may be slow for the browser to download and run.
Sure, you can optimize this bundle by code splitting. But there will still be some upfront cost the browser must pay just to launch the site.
<!-- A typical SPA page -->
<head>
<script src="/app.js"></script>
</head>
<body>
<!-- This page has no meaningful content until app.js loads -->
<div id="app"></div>
</body>
Islands architecture
Islands architecture, the architecture used by Astro, also uses components. Unlike a single-page app, though, these components are not bundled together into a JavaScript bundle.
Instead, each component is treated like an independent mini-app that exists in isolation from all the others.
For example, if your page has a JavaScript-powered nav bar, that would be one mini-app. If it also has a JavaScript-powered image carousel, that's another mini-app. And so on.
But if these components aren't bundled, how do they get included in the project? I'll explain that in the next section.
<!-- Islands architecture -->
<body>
<MyNavBar /> <!-- navbar mini app -->
<main>
<MyCarousel /> <!-- carousel mini app -->
<div class="content">
<!-- more page content... -->
</div>
</main>
</body>
Server-rendered components
Astro is primarily a static-site generator. It works with most UI libraries that support server rendering of components including Vue, Svelte, Preact, React, and Lit.
So when Astro builds your app, each of the JavaScript components is loaded server-side and the content is "snapshot". This snapshot is added to the static page.
Server rendering is a feature that is not particular to Astro, but in SPAs this is an optional feature, while in Astro this is a crucial feature as we'll see next.
<!-- Development content -->
<body>
<MyForm /> <!-- JS component -->
</body>
<!-- Shipped content -->
<body>
<form> <!-- Server rendered JS component -->
<input type="text" >
<!-- ... -->
</form>
</body>
Progressive hydration
Here's where the magic of Astro comes together - through the combination of islands architecture, server-rendered components, and progressive hydration.
Since our page is divided into server-rendered mini-apps, the interactivity layer (the JS) can be loaded independently and only when it's needed.
For example, you may have an interactive form. This form is further down the page, out of the viewport.
The form is server-rendered, so we see it on the page. However, the expensive JavaScript won't need to be loaded until the user has scrolled it into view.
This is what's meant by "progressive hydration" in Astro - we only load JavaScript as it is needed, when it is needed.
Hopefully, you're now getting the idea about Astro's architecture. If you'd like a deeper dive, check out this video I made: Why Astro will be your favorite web app framework.
Setting up a Vue + Astro project
Now that the theory is out of the way, let's see it in action!
To begin creating an Astro project, we'll first create a directory:
$ mkdir vue-astro
Then run the Astro installation wizard:
$ npm init astro
The installation wizard will allow us to select "Vue" as our chosen framework. This will create a boilerplate project including Vue components.
Astro components
Astro pages are kept in the src/pages directory. In a default install, we see a file index.astro, shown below.
src/pages/index.astro
---
import VueCounter from '../components/VueCounter.vue';
let title = 'My Astro Site';
---
<html lang="en">
<head>
<!-- ... -->
<title>{title}</title>
</head>
<body>
<main>
<!-- ... -->
<VueCounter client:visible />
</main>
</body>
</html>
Astro has a single-file component style, like Vue but with a few important differences.
Firstly, at the top of the file, we see what appears to be frontmatter i.e. content delineated with ---
. This is the JavaScript that gets run server-side. This does not get sent to the client.
Here we can see two important things: firstly, we're importing a Vue component (you can import components from any supported framework here). Also, we're setting a value, title
.
All the variables declared here are available in the template. You'll notice the title being interpolated in the template in a JSX-like syntax.
src/pages/index.astro
---
...
let title = 'My Astro Site';
---
<html lang="en">
<head>
<!-- ... -->
<title>{title}</title>
</head>
<!-- ... -->
Next, notice that the component declared in the template.
By default, components are not interactive in the client and are simply server-rendered by Astro.
If we want to make a component interactive, i.e. load the JavaScript, we need to give it a directive telling the client when to load it.
In this case, the client:visible
directive tells Astro to make VueCounter
interactive when the component becomes visible in the page.
If and when this happens Astro will request this component's JS from the server and hydrate it.
---
import VueCounter from '../components/VueCounter.vue';
...
---
<html lang="en">
<head><!-- ... --></head>
<body>
<main>
<!-- ... -->
<VueCounter client:visible />
</main>
</body>
</html>
Loading Astro
Let's now run Astro's development server to see our project.
npm run dev
In the page source you'll see there is no JavaScript bundle in the document! We do see the server-rendered Vue component, though.
We also see that Astro added a script to the bottom of the document body. Here it loads a module hydrate the Vue component.
This module will download both the Vue component and the dependencies (the Vue framework) without blocking render.
index.html
<!-- Page source -->
<body>
<!-- server rendered component -->
<div id="vue" class="counter">
<button>-</button>
<pre>0</pre>
<button>+</button>
</div>
<!-- snippet added to hydrate the Vue component -->
<script type="module">
import setup from '/_astro_frontend/hydrate/visible.js';
// ...
</script>
Why Vue + Astro may be better than Vue SPA
To see why Astro can beat a single-page app in terms of UX, let's do a simplified breakdown of what happens when the site loads.
index.html is loaded. It has no JS bundle, but it includes your server-rendered components so the user can already see your site content - it just isn't interactive yet.
Any JS that is required for components will now be asynchronously downloaded as a series of independent scripts.
Once these scripts are downloaded, they will be parsed and run. Now interactivity is available.
Now lets imagine we re-built this site as an single-page app. How would it load now?
index.html is loaded. Since the page contains no content, the user can't see anything. The browser will begin downloading the bundle.
Once the JS bundle is downloaded it is now parsed by the browser. The user still can't see anything.
Once the JS bundle has been parsed and run, and the page content is now generated. The user can now see and interact with the app.
Long story short - the Astro site will provide visible content almost straight away, unlike the SPA which needs to download and run a JS bundle first.
(The Astro app will provide interactivity slightly sooner as well, as it probably doesn't need to download as much JS as there is no SPA shell, router, etc.)
Final thoughts
Astro's architecture may be a better option than a single-page app since it makes content visible without JavaScript and only loads the JS as it needs it.
Theoretically, a single-page app can achieve something similar through a combination of prerendering and code splitting. The difference is that Astro sites are optimized this way by default as you need to opt-in to interactivity and JS.
This is why people are getting rocking Lighthouse scores out-of-the-box.
Of course, not every app will benefit from this architecture, as SPAs are just a better fit for certain types of apps e.g. highly dynamic and interactive apps. So we won't expect the SPA architecture to disappear.
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...