Building a high performance website
Feb 13, 2023
I’ve had this blog for quite a while, although I’m not a professional content creator I do my best at documenting my work,
It helps me organize my thoughts and hopefully it might help others passing by.
But the intrinsic motivitation for writing content is that I build web products,
and this blog is just me prototyping with whatever web-related unicorn I want to play with.
Today I want to see how I can improve my blog’s lighthouse score,
if you don’t have web-dev experience nor know lighthouse you’re probably not ready to go down the rabbit hole yet,
in this case I suggest to start from here:
Custom vs fandom
Web development (like most other fields) is made of trends,
in this sector many JS frameworks are born and die in the lifespan of a butterfly, but some eventually stick around.
Last years have been dominated by React, and the SPA bandwagon, these are awesome if you need to build applications,
nethertheless they bring in many issues in the context of optimization:
- The amount of javascript source files you need to download is ludicrous.
- Browser side routing are frowned upon by crawlers.
- Virtual dom frameworks abstract away from the real dom through js, this translates to extra resources being spent by the browser and ultimately the device.
We certainly don’t need SPA capabilities to display a blog,
but building a custom process that generates html would mean reinventing the wheel in terms of DX and scaffolding…
So I ended up choosing Astrojs,
which transforms a component architecture into a static resource website with a state of the art compilation stage.
being specifically built for displaying static content, it fits my requirements, and has a lot of excellent docs that go with it.
First try
Actually I cheated a little with this screenshot, on first try I was still missing a lot of stuff (fonts, images, etc.),
but anyway, later, I managed to include everything and still hit 100.
Astrojs
The workflow that astro sets up for developing is great, each part of the app is divided in components, the dev environment is powered by vite (blazingly fast), while the production build is a crafty custom process.
Routes are handlend on a resource base, during development you can define them similarly to nextjs using getStaticPath
and then transformed during the build.
There is also a SSR variant, but I didn’t use it.
I included many addons to improve performance and DX:
- Styles are handled in SASS.
- Using tailwind through addon.
- Content is written in MDX (pimped with rehype and remark).
- Images are dealt with the astrojs Image component.
- Prefetch has it’s addon.
- Partytown for offload the main thread.
- Critter to inline only the required css.
- Sitemap generated automatically by it’s addon.
- PWA is handled custom with workbox.
Using addons is plug and play, everything fell into place quite easily, moreover custom additions are really easy to implement.
Tuning
Following you’ll find some tuning I went through to achieve high performance.
Moving parts
I stripped extra scripts to the bone but here’s how I managed to build a custom menu.
The menuIcon component is dealing with the opening/closing of the drawer, and the main-content component reacts with some width adjustment.
Both components hook up to a nanostore in order to keep that nice separation keen to every component architect.
One thing to watch out for is moving nodes around during first render, this slows LCP (Largest Contentful Paint)
which is an essential metric lighthouse uses to define the score.
In this regards I just check a flag set in sessionStorage to align the state of the drawer (‘open’ | ‘close’).
Furthermore, all the custom script logic loads with defer attribute set to avoid blocking the page parsing.
Non blocking fonts
Custom fonts on a site is something you’ll probably need,
but loading the fonts synchronously dramatically increased the size of my page.
Import through fontsource get added directly as a stylesheet and
together with critters, this translates to copying the font source directly in you main html.
In Astro this can be avoided simply importing the font inside an async
script element,
this will parallelize the execution of the script which add the style node for the fonts:
<script async={true}>
import "@fontsource/kiwi-maru"
</script>
instead of
import "@fontsource/kiwi-maru";
This tweak reduces the page size by 92K with no UI visible difference.
32K async.html
144K sync.html
Critters
This tool statically analyzes the page source and only imports the required css inlining it the html page,
It reduces the total amount of CSS and drops extra network transactions.
The integration with astro is seamless.
Partytown
it facilitates the loading of all non critical scripts through the use of a service-worker it controls.
I use it mainly for Analytics.
Prefetch
Wouldn’t it be nice if we could preload the page resources we need in order to have them ready when the user navigates to them?
Such a functionality exists in the browser, unfortunately it’s not supported by the anchor element.
But this is easily replicable with a script, and astrojs has got you covered with the prefetch addon, you just need to add a [rel]='prefetch'
attribute to the a tag that nees prefetching.
Under the hood the addon will search for all elements with this attribute and, if the connection is not 2g/3g, it will make a request that will cache the page.
PWA and service worker
The PWA manifest is dropped directly into the static
dir and imported in the root page.
To deal with service worker I used the workbox-cli with generateSW flow
.
Then, through the Astro plugin integration api, the import logic is written in my homepage index.
If you want to know more about that you can read the dedicated post about it.
Wrapping up
The experience of optimizing this blog was fun, going to the basics I got to tweak a lot of low level aspects.
Using Astro I was able to build something I’m really satisfied with (especially from an architectural point of view),
and it’s wasn’t even that hard, you just have to get the fundamentals write :).
Does my site now qualify as blazingly fast?
In some regards yes, but there is still some work I want to do with CDNs,
stay tuned if you’re interested, and thanks for passing by!