Bit of Performance

I was digging into some of the performance issues during my work for a few weeks, and I thought that it can be interesting to see how those could be applied to my site. I was already quite happy with how it performed (being just a small static site) but managed to find a few areas to improve.

nitial State

Before I’ll go into what I’ve done to make my site faster, I’ll try to describe briefly what I had before:

And that’s basically it.

hat I Wanted to Improve

I was kinda happy with how everything was, but I still experienced a few things where I felt things could be done better: the initial load of the pages, especially how the fonts were loaded, and the consecutive navigation between the pages.

irst Page Load and the Fonts

I would consider my site on the lighter side, with the fonts being the heaviest part. During the last rewrite of my site I seriously considered dropping them for the default ones, but after some prototypes and experiments, I found that with the current minimalist design dropping such a significant part of the whole picture was quite devastating. And I really got used to the font I use right now.

Even though I subsetted the font and had used woff2, I could still experience sometimes, especially for all the headers, so looking into how I could reduce it was a priority.

There were also some other aspects of the initial page load, like an order of all the resources which I didn’t optimize before, but the fonts were an obvious bottleneck.

Then, there was that one thing that I’ve noticed when going through my site’s pages and measuring its performance using Chrome dev tools.

Even though my internet connection is quite fast, I still felt that going from page to page was quite slow. After looking at what happens, I have noticed that browser extensions can affect the performance of web pages quite significantly. In my main browser profile, for example, their effect could cause up to 1–1.5 extra seconds before the DOMContentLoaded event would happen!

Browser extensions are initialized for every page you, and the parsing and evaluation of those extensions’ JS can add a lot to the page’s load time.

Basically, on mobile pages, we have latency and slow JS, and on the desktop, even with a decent internet connection, we could have all the extra work happening by all the browser extensions. So I have tried to see how I could manage this as well, as it is not very fun when your highly optimized static site gets slowed down so significantly by outside factors.

hat I Have Done

I won’t go into the details of implementation for everything I’ve done to make things faster but would try to at least briefly list all the methods I have used. Think of it more as of a changelog, and maybe I’ll try to cover some of the more interesting parts of it in the future articles (let me know if you’d want me to cover something specific!).

reloading the Critical Resources

The thing that I saw helping me with the FOUT was adding the <link rel="preload"> tags for my fonts.

Here is how it looks for this article, for example (for other pages it could be different, I’ll talk a bit about it later):

<!-- First, all the preloads -->
<link rel="preload" as="font" type="font/woff2" href="/s/21Cent-Regular.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/s/21Cent-Black.woff2" crossorigin />
<link rel="preload" as="style" href="/s/style.css" />
<link rel="preload" as="font" type="font/woff2" href="/s/21Cent-Italic.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/s/21Cent-Bold.woff2" crossorigin />
<link rel="preload" as="script" href="/j/scripts.js" />

<!-- Then, the resources themselves -->
<link rel="stylesheet" href="/s/style.css" />
<script src="/j/scripts.js" defer></script>

Important things to note about those preload :

  1. The resources would be requested in the order you list them there.
  2. The preloading would be triggered as soon as the browser would see those links while parsing the HTML.
  3. A number of simultaneous requests a browser can make to one origin can be different in various browsers, but the minimum for the modern ones is 6, so it is important to think what to choose for the first batch.
  4. Loading the page’s HTML takes a request as well! So if you’re loading static assets from the same origin as the HTML, only 5 slots would be available for the first batch at the beginning, with the sixth being only available after the HTML would be loaded. For the smaller pages this can be insignificant, but for the bigger pages, this can cause a difference.

Given all of that, I have placed the preload links in the order above: first two main fonts that are present “above the fold” for most of the pages, then the page’s styles, then the two remaining fonts, and the page’s scripts being the last.

ont-Display

Fonts being loaded before the styles is very important if you want to minimize the FOUT, as soon as the styles would be loaded, they would be applied even if there are no fonts, and then the font-display would handle what would happen.

After playing a bit with font-display for my site, I have decided to use swap value for it. For some other sites other values could fit better, but for my case, that was enough.

ont Subsetting

While I have subsetted my fonts before, I still kept both all the Latin and Cyrillic symbols in each font file. As the English version of my site is the default one right now, I have decided to reduce the size of the font files even more by the Cyrillic glyphs into their separate files and then applying them by utilizing the unicode-range property.

Here is an example for one of the fonts I’m using:

@font-face {
  font-family: '21Cent-Regular';
  font-display: swap;
  src: url('21Cent-Regular.woff2') format('woff2');
}

@font-face {
  font-family: '21Cent-Regular';
  font-display: swap;
  /* All Cyrillic glyphs, plus `ё` and `Ё` */
  unicode-range: U+0410-044F, U+0401, U+0451;
  src: url('21Cent-Regular_cyrillic.woff2') format('woff2');
}

ubsetting and Preloading

Splitting Cyrillic glyphs into the separate files means having twice as many files, but as not every page uses them, I do not use preload links for those. I added them only to the pages of the Russian version of my site and on the homepage, where it is guaranteed that they would be used.

Another thing to note: making subsets means that overall size of both fonts would be bigger than for a non-subsetted one, so if you’re sure all your pages would use most of the subsets you need, splitting could be less beneficial or could even be harmful by introducing some extra stuff to load.

ostponing The Counters and doNotTrack

I use two counters for my site: one for the MyFonts views, required by the font license, and one for my web analytics. Having them load before everything else didn’t give me anything, so postponing them after the load event did make both DOMContentLoaded and load events to happen much earlier for my pages.

I have also used <link rel="preconnect dns-prefetch"> for the MyFonts counter origin, and while I found the effect of this to be quite minimal, I have decided to keep it anyway. You may have also noticed that I do not use the same preconnect/dns-prefetch <link> for my web analytics — that is because it is better not to use this method for the resources that are not guaranteed to load on the page — and I have decided to follow the doNotTrack header and if I see that user prohibits it, I don’t even try to load the web analytics.


There were probably some other minor things I forgot to mention that helped with the initial load, but those above had the most impact.

Here is a list of on this topic I have read while playing with all of the above:

rogressive Navigation

A thing that I have spent much more time than on playing with headers — implementing JS-based navigation between inner pages of my site. This approach can also be known as “PJAX”, and is often used in Progressive Web Applications context: instead of allowing browser to handle the page change when you click on a link, we can get the target page’s content by fetching it asynchronously and then replacing our page’s title and layout with the new ones.

This can be enhanced in a lot of ways, and there are already a lot of projects that implement different parts of this approach, but I was interested in implementing everything from scratch. Not to dive deep into the details, here is what I’ve done:

  1. I have implemented loading of JSON instead of HTML pages that contain all the HTML of the changed content for the page, alongside some metadata. Everything is generated inside Hugo, it wasn’t that hard to set up the generation of .json files for each page, but I had to restructure the layouts for my page a bit.

  2. I’m handling the links via a bubbling onclick event on the document, fetch the .json with the window.fetch and using pushState to handle the page’s state. As all of this is basically a progressive enhancement and the site would still work with JS disabled or any features absent, I can be a bit relaxed over the compatibility.

  3. The state handling for navigation wasn’t completely transparent and has a bunch of things going on with it. Example: whenever we need to navigate to a different page that has an anchor, like from /foo/ to a /bar/#baz, and at the same time handle the browser History properly, and also trigger the :target for the anchor in question, then things become a bit complicated as we need to use a bunch of extra logic with setting the location.hash and replaceState instead of pushState. Maybe I’ll write about this in more details one day.

  4. I’m caching the fetch result in localStorage so I wouldn’t need to do the requests when I already have the content, and also caching the DOM of the replaced content so I wouldn’t need to recreate it whenever I’m using the browser History.

  5. I’m using an “instant” preloading of the pages when people hover over them. This is kind of a common pattern these days, and with localStorage, I can make sure those requests wouldn’t repeat and could be useful in the future even if the user won’t visit the hovered pages right away. Similar to clicks, I’m handling this via a bubbling mouseover event, and I do not do any fallbacks for touch interactions, as I don’t want people on mobile connection to download extra stuff and the profits there for this method are a bit lower overall.

  6. As I’m storing everything in localStorage and do not do extra requests whenever I already have the content I need, I need to have some way to invalidate the cache. I decided to have a versions.json (per language) with short sha256 sums for the content of each page. My site is not that large and for my English version of a site this file weights just 1.8kb (even less gzipped), so I can lazyload this JSON on the initial load, making it easy to compare the current versions with those in the cache.

  7. To make sure this kind of dynamic navigation wouldn’t have a too big of an impact on accessibility, I’m using a aria-live="polite" attribute for my page. In my testing this is enough to get a better result than without it in VoiceOver, but there should be more testing of everything for sure, and I wouldn’t recommend you to use this method for your sites without a thorough testing, as otherwise, you may break the experience for those who rely on accessibility tools. If you happen to notice any problems with accessibility on my site, I would be happy to know about them and look into how I could fix it, of course.


Overall, I’m pretty happy with the result and can feel the difference, even if the PageSpeed test and similar doesn’t show almost any difference (~100 desktop/~92 mobile, with the web analytics the only part lowering the score right now, oh well).

There are a bunch of projects that implement similar approaches, but I have tried to do everything from scratch in order to understand how everything works, what are the potential problems and use cases for all of the technologies beyond.

Here is a list of useful resources I stumbled upon while implementing everything above:

hat Is Next?

Well, that was a lot of stuff! I’m satisfied with how both the initial load works on my site, and how the following navigation now feels much more snappy. The JS for the navigation, in the end, is maybe a bit larger than I have anticipated — around 220 lines of code, and it is far from optimal either, as there are probably a lot of edge cases that I didn’t cover. But, for now, I’m very happy with what I have in the end.

I have also learned a lot of new things, a lot of nuances of creating a navigation system like this, and in the future even if I’d choose one of the already built open source solutions, I would know where to look when deciding which one to choose, or what to adjust for the better experience.

For the current one, I think if I’ll make another pass over everything, there are a bunch of things I would like to experiment on:

inal Words

Implementing such thing from scratch is a thing I highly recommend you to do — whenever you have a task, try to first do the basics by yourself. You’ll learn some new things and would see if your prototype is good enough, or if there are enough edge cases to go and grab an existing solution.

However, try to have some limits over how much time you’ll spend on the prototype — the less the better, as you wouldn’t want to throw away a lot of work if you’d decide to switch. And you wouldn’t always have a lot of time for experiments — but, even then, I still urge you to try and do them at least sometimes, as it can lead to a better understanding of the field, and sometimes would lead to much quicker solutions for the tasks you have at hand. And all of that can be fun as well, and doing experiments could motivate you to work more efficiently — so try it! And it would be totally ok if this approach wouldn’t work for you — everyone has their own ways to work, there are no perfect ones.

And as a last note: hey, now that my site should be even faster to navigate through than ever before, why not give it a go and see what I have written in my blog? There are a lot of articles about CSS and various weird experiments, as well as a lot of other stuff!

And if you’ll stumble over any problems or bugs in how all of that works — tell me and I’ll fix them.