Your javascript webapps don’t actually need javascript.

blazing-fast.jpg

Daniel Ricciardo doesn’t have anything to do with this article.

Tl;dr: I made my personal website almost twice as fast by porting it from Nuxt to Astro. I made it so that zero JavaScript is sent to the browser[1] .

Some background #

I haven’t touched my website in 3 years according to the git log. I didn’t really want to change it, but I did want to play with some new technologies. The webdev landscape is constantly changing, and there are some cool new things to discuss.

Let’s travel to an island. #

ab67616d0000b2734bd6b3b9ca4e1f4adb351d45.jpg

Album cover for Sainté’s album called Vacation.

Three years ago, server-side rendering (SSR) was all the hotness. That’s partially why I built my personal website in Nuxt (which is like Next.js but for Vue). I wanted to learn Vue so I gave Nuxt a shot.

There are clearly advantages to SSR. But over the last year or so folks have realized that there are also downsides. The most major one is that SSR applications need to do roughly the same work twice.

Under the hood of SSR applications #

This is a bit of an over simplification.

At an extremely high level, here’s how SSR applications work:

  1. First the backend must build the HTML for the initial page load.
  2. A frontend framework (like React or Vue) is loaded on the client.
  3. The data on the backend is synced to the frontend (through a mysterious process called hydration). Hydration usually involves including a large chunk of JSON in the <head> of an SSR application. This is the only way the frontend framework can know the state of the work that was already done on the backend.

This is the crux of the issue: we’re sending entire frontend frameworks and a lot of data when we might not have to.

Introducing islands #

Most websites just aren’t that reactive. Your average portfolio website, blog, or product page doesn’t have that much “reactivity.” So why are we sending down entire frontend frameworks to the clients? What if we could think of the reactive parts of websites as “islands” and only send the JS code needed to make those portions reactive.

islands.png

Image from Astro’s documentation.

Let’s take a look at a product page like this Nike shoe product page. The only real reactivity on the page is that as I pick different shoe sizes and colors, it updates to show the availability. The header, product photos, product details, social media space, and footer all remain unchanged regardless of what I do on the product page. If this page was built using a conventional SSR framework (say using Next) then we’d be sending down a lot of React code for components that are never reactive. Besides just the cost of transferring data, there is a computational cost associated with hydration and virtual DOM reconciliation.

Though other frameworks (like Next and even React server components) are starting to play with the idea of islands, Astro is one of the first frameworks to really pioneer the concept. This doc page gives some more background on islands.

By default, Astro doesn’t send your component’s JS to the client unless you specify it to.

Here’s what an astro file looks like when we do not want to send the component’s JS to the clients:

---
import Header from '../components/Header.vue';
// Fetch data from an API or a headless CMS like Contentful 
---
<!-- 100% HTML, Zero JavaScript loaded on the page! -->
<Header />

And if we add client:load to the component then we tell Astro to not only render the component on the server but send it down to the client.

---
import Header from '../components/Header.vue';
---
<Header client:load />

I don’t have time to learn another JS framework #

Good news: Astro is a “meta” framework. It lets you utilize other frameworks (such as React / Vue / Solid / Svelte) inside it. So I didn’t actually have to rewrite large chunks of the codebase.

Some things I had to address when porting over to Astro:

  1. Libraries. Tailwind.css is different three years ago than it is now, so I had to fix some styling issues.
  2. Packages. I was using a Vue markdown component which was causing issues with Astro’s build system. Instead of fighting with it, I decided to switch over to marked.js which parses markdown in the parent .astro file and then passes the HTML into the child Vue component. I then just set the parsed HTML in the v-html of <div v-html="props.body"/> (security friends look away). Astro is relatively new so if you’re using some fancy packages, be mindful that you might have to fight with them to get them working.
  3. Project structure. Nuxt and Astro share a fair amount of the folder structure (docs), but there were some minor differences with how wildcard slug URLs are handled.
  4. No /api folder. In Nuxt (and Next) there is a top-level api folder where you can place serverless functions. Astro doesn’t yet have that. I wasn’t using serverless functions in my project, but it’s just something to be mindful of.
  5. No middleware (yet). Next and other frameworks have a middleware folder that let’s you handle auth, URL redirection, and other common middleware handler tasks. Astro doesn’t have that yet.

As an explicit goal I wanted to see if I could get away with sending zero JS to the clients with no difference to the UX. The only piece of reactivity on the homepage was the header links. When you hovered over the homepage link, it says “Homepage”, and when you hovered over the pen logo it says “Blog”. Vue was being sent to clients just to support this little feature. I have changed the header links to be normal button links. They aren’t reactive anymore, but this allows me to send zero JS to the clients and I’m pretty sure these button links are much better for accessibility.

Show me the numbers #

chrome-time.png

Screenshots of Chrome dev tools loading the homepage: left is the new website, and on the right is the old website[2] .

Metric New website (Astro) Old website (Nuxt)
Requests 8 45
Data transfer (homepage) 42.5kb 1.6mb
DOMContentLoaded 344ms 610ms
Load time 795ms 1.89sec

I could use an actual benchmarking tool to get more statistically sound data rather than just refreshing the deployed webpages and looking at the networking numbers. But you don’t need to be a stats nerd to realize that those numbers are fairly different. And here’s what’s important: reloading the website feels faster. Especially after it is loaded after a while of being inactive (since Vercel needs to do a cold start).

The goal of sending zero JavaScript to the browser was also accomplished. Even though the application is fully written in a JS framework (Vue), you can disable JavaScript in your Chrome DevTools, hit refresh, and the app will function exactly the same as having JavaScript enabled.

Check out my website for yourself: shahzeb.co.

Edit: Since publishing this post I’m also prefetching content for the /project/* URLs. A few more requests are made on the homepage but it doesn’t really impact page load time. As soon as you click a purple project, the page changes without much loading.

[1]: I am using Google Analytics but it is totally optional and can be disabled using Ad Block.
[2]: These numbers do not include the cold-start times which can be much longer.

 
14
Kudos
 
14
Kudos

Now read this

Add analytics on links in React.

The problem: # Recently I joined a team with with a large React front-end. I was tasked with figuring out how to add analytics to a ton of buttons and links throughout the application. I wanted an easy way to add analytics without having... Continue →