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:
- My site is fully static and hosted on Netlify, generated by Hugo with some help from Gulp and some JavaScript I wrote.
- I do not use any front-end frameworks on it, and the only JS that I have on a client-side is an Algolia search and a Prism.js code syntax highlighting.
- I do not have any code-splitting etc, so all the CSS is loaded inside one file and is compressed with CSSO.
- I used web fonts — four files for those, and I have used only woff2 format (I’m ok with everyone else getting the fallback font) and used
font-display
with them. - I rarely use images, and when I used them I pass them through ImageOptim (and now use Squoosh as well).
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 FOUTGo to a sidenote 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.
avigation Cost, Latency and Browser Extensions
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 visit, 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 the mostGo to a sidenote 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 linksGo to a sidenote:
- The resources would be requested in the order you list them there.
- The preloading would be triggered as soon as the browser would see those links while parsing the HTML.
- 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.
- 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 separatingGo to a sidenote 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 articlesGo to a sidenote on this topic I have read while playing with all of the above:
- “Font-display” playground by Monica Dinculescu — a nice explainer and a demo of how different values of the
font-display
property work. - “Building the DOM faster: speculative parsing, async, defer and preload” by Milica Mihajlija — a good article about the difference between async/defer and a bit about the link preloading.
- “The Critical Request” by Ben Schwarz — a nice article about the resource loading priorities, preload links and so on.
- “Preloading Fonts and the Puzzle of Priorities” by Andy Davies — a bit more deep dive into the preload links.
- “The Web Fonts: Preloaded” by Zach Leatherman — another article on preload, with some benchmarks.
- “What forces layout/reflow” by Paul Irish — I have changed some minor stuff in my JS to prevent extra layout thrashing, but those were quite minor to mention there.
- “Make your Google Fonts render faster” by Ivan Akulov – if you’re using Google Fonts, this tool could help you with adding the preload links for them very easily.
- WebPageTest — quite a useful service to test your requests, shows a lot of information.
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:
-
I have implemented loading of JSON filesGo to a sidenote 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. -
I’m handling the links via a bubbling
onclick
event on thedocument
, fetch the.json
with thewindow.fetch
and usingpushState
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. -
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 thelocation.hash
andreplaceState
instead ofpushState
. Maybe I’ll write about this in more details one day. -
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. -
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 bubblingmouseover
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. -
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 aversions.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. -
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:
- The PRPL Pattern by Addy Osmani — the thorough description of this pattern.
- PRPL Pattern at Gatsby docs — Gatsby.js implements a lot of the same things I have implemented by myself, maybe except for the localStorage caching (from what I have noticed), so if you have a blog on Gatsby, you could have a lot of things I have described already.
- pjax npm package by Maxime Thirouin — implementing the approach without using the JSON files, just by fetching the
.html
pages and doing some JS stuff to replace the changed parts. I didn’t try to use it, but it seems interesting and you can see if it would work for you. - https://instantclick.io and https://instant.page by Alexandre Dieulot — projects implementing the “hover to load” approach. Also didn’t test those, but the instant.page looks like a nice way to quickly do this by utilizing the link prefetch.
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:
- Implementing offline and more caching via service workers. I feel like what I did already could help me a lot with this, but some first glances I did at the service workers made me think they’re maybe a bit too cumbersome for my purpose right now.
- There are some issues left with how scroll position is saved during the History navigation, maybe I’ll look into using
History.scrollRestoration
where it is available in the future. - There are some minor bugs related to the Algolia Search in relation to the new navigation, I think I’d like to rewrite the search form from scratch. It is also completely inaccessible to screen readers right now, to the point I had to hide it from screen readers for good, so going back and looking into how to make it accessible as well is totally a goal for me.
- In Webkit/Blink-based browsers the performance bottleneck for my site seems to be layout recalculation. This is highly possible because of all the experimental CSS I’m using for it, so I would like to find what exactly causes this problem and if I could work around it somehow without compromising on what I did with CSS there.
- Implement automatic subsetting for the fonts. This can be a bit too hard, especially given that I need not only the regular glyphs, but also alternatives, but if I could find a way to do this not manually, this could be great.
- I’d like to rewrite all the JS I wrote for my site — both older stuff for the search, and the newer stuff I wrote for the PJAX — a lot of those are quite messy right now, and there are sure a lot of places which could be done better.
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.
Let me know what you think about this article on Mastodon!
Published on with tags: #Practical #Blog #Performance #JS #Fonts