JavaScript Efficiency War: Astro.js vs Next.js
Kairat
Posted on February 21, 2024
Astro has been on the market for a while but it's still considered a relatively new library. In 2023 there were a lot of talks about it and I decided to give it a try.
Before I was primarily using React and Next 13 but most of the applications I was working on were informative simple websites with mostly static data. That is why Astro seemed very attractive as it is the perfect library for such use cases.
Like most people who tried Astro, I liked it and in this article, I want to particularly discuss how Astro impacts the performance of your website.
To start, I want to talk a bit about the new architectural pattern that Astro is trying to popularize. It's called island architecture and Astro docs have a separate page explaining this concept.
Here I just want to briefly highlight the main ideas behind it so you have more context on why Astro is so good at performance.
If you've never heard of this technique (architectural pattern), there is a chance you might know it by a different name - partial or selective hydration.
The main idea behind it is relatively simple.
First, let's define what's an island.
Basically, an island is any interactive UI component on the page.
An island always runs in isolation from other islands on the page, and multiple islands can exist on a page.
So, now let's move on to non-interactive components on your pages.
All other UI components - non-interactive ones are static, lightweight, server-rendered HTML & CSS.
And now what we have in the end - static components rendered on the server along with dynamic ones - those that can be "hydrated" on the client.
What does this all mean?
React, Next.js and other SPAs use a monolithic pattern where the entire website is one large JavaScript application (not in the case of Next.js) and we end up shipping a lot of unused JavaScript to the browser which we don't even gonna use. With these libraries we can't selectively (partially) and strategically hydrate certain components, instead, we hydrate the entire app.
If all of it does not make sense to you, let me show an example of how it works.
Let's say we have a web page with the following structure.
If we would build the following static page using Next.js, we would render it on the server and send JavaScript to the client to hydrate dynamic components, right? The catch here is that we not only gonna send the JS required for Header & Carousel components. but also JS for static components as well and we don't need it cause we know those 3 components are static and there is no need for us to hydrate those on the client.
That's why we have Astro, and what would happen if we were to build this same page with it?
We strip all non-essential JavaScript from the page (it's done automatically) which slows down the website and instead, we only send JS needed for dynamic components' hydration.
I hope it all makes more sense now, but if you are still feeling a little bit confused, I highly recommend watching this video from Vite Conference by Nate Moore.
To give you even more visualization, I built this super simple web page where users can enter their email and after clicking the subscribe button confetti is shown.
I built it twice - using Astro.js and Next 13 static export (can be done by specifying output: 'export' in the next configuration file).
That's how the code looks if using Astro.js
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<div class="w-screen h-screen bg-blue-800 flex justify-center items-center">
<div class="flex w-[60vw] h-[70vh] bg-neutral-300 rounded-3xl px-8 py-4">
<div class="flex flex-col basis-1/2 gap-y-4">
<h1 class="text-4xl">Stay updated</h1>
<h2>Join 60+ product managers receiving monthly updates on</h2>
<ul class="list-disc list-inside">
<li>Product discovery and building what matters</li>
<li>Measuring to ensure updates and a success</li>
<li> And much more!</li>
</ul>
<h3 class="pt-8">Email Address</h3>
<form id="newsletterform" class="flex flex-col gap-y-2">
<input required type="email" class="w-[90%] p-4"/>
<button id="subscribeBtn" type="submit" class="w-[90%] p-2 bg-red-400 hover:bg-red-500">
Subscribe
</button>
</form>
</div>
<div class="basis-1/2 rounded-3xl bg-pink-400 overflow-x-hidden">
<img src="TestImg.jpg" alt="Alternative Text for Image" class="h-full object-cover"/>
</div>
</div>
</div>
</body>
</html>
<script>
import confetti from 'canvas-confetti';
const form = document.getElementById('newsletterform');
form?.addEventListener("submit", (e)=> {
e.preventDefault()
confetti()
})
</script>
Here is the Next 13 version:
page.tsx
import Form from "@/components/button";
export default function Home() {
return (
<div className="w-screen h-screen bg-blue-800 flex justify-center items-center">
<div className="flex w-[60vw] h-[70vh] bg-neutral-300 rounded-3xl px-8 py-4">
<div className="flex flex-col basis-1/2 gap-y-4">
<h1 className="text-4xl">Stay updated</h1>
<h2>Join 60+ product managers receiving monthly updates on</h2>
<ul className="list-disc list-inside">
<li>Product discovery and building what matters</li>
<li>Measuring to ensure updates and a success</li>
<li> And much more!</li>
</ul>
<h3 className="pt-8">Email Address</h3>
<Form />
</div>
<div className="basis-1/2 rounded-3xl bg-pink-400 overflow-x-hidden">
<img src="TestImg.jpg" alt="Alternative Text for Image" className="h-full object-cover" />
</div>
</div>
</div>
);
}
button.tsx
"use client"
import confetti from 'canvas-confetti';
export default function Form(): JSX.Element {
return <form id="newsletterform" onSubmit={(e) => {
e.preventDefault();
confetti();
}} className="flex flex-col gap-y-2">
<input required type="email" className="w-[90%] p-4" />
<button id="subscribeBtn" type="submit" className="w-[90%] p-2 bg-red-400 hover:bg-red-500">
Subscribe
</button>
</form>;
}
I built it using the npm run build command and then started the application in production mode (npm run start for Next and npm run preview for Astro).
Now, we go to the network tab to see what requests we make to display this page. Particularly we are interested in JS requests.
And what do we see?
In Next.js, we have 5 requests
While in Astro, it's just one
In total, we made 5 requests in Astro (HTML document, 1 CSS file, one JS, 2 images (body and favicon) and in Next 9 - +4 JS files.
If you want to see, what those JS files consist of, you can use bundler analyzer tools.
I used this one for Astro - vite-bundle-visualizer.
And for Next, there is actually a package built for it - @next/bundle-analyzer
As you can see from this image, Next.js comes with a bunch of default packages it needs for proper work - and we, as developers, could benefit from those a lot but in more complex apps with more features while simple static informative websites don't need those.
In terms of Lighthouse performance, Astro is better here as well (just a bit and of course it's gonna vary a lot based on multiple factors)
I know it is a super simple web page and in reality, you would have much more complex structures but it still shows the difference between these 2 frameworks and how Astro improves performance by stripping non-essential JavaScript code.
In the end, just wanna say that I am gonna continue using Next 13 as well as Astro. The main point of this article was to show you that as developers we shouldn't rely on one tool and be flexible depending on the situation.
And that's it, guys.
I hope that you have learned something new today!
I would appreciate it if you could like this post or leave a comment below!
Also, feel free to follow me on GitHub and Medium!
Adios, mi amigos)
Posted on February 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.