Speeding up my sites

I’ve had my website since June 2017. Found a nice template, filled it with content, customized it a little and just published it.

As time went by I added a few articles, played with the aboutme page and did a few tweaks.

But it was always pure HTML and CSS with no frameworks, no templates, and no code reusability. I thought it was just unnecessary to burn through time and effort. I can replace the reusability with search-and-replace, right?

This was the case for my other site doprcavky.cz where I bought a template and did the same - just copied some files and filled them with content.

I wanted to play a bit with Hugo SSG and Vercel. So a perfect use-case to try this on some static sites!

I’ve focused on desktop performance instead of mobile

click the images to view the full report


After some optimization on hosting this whole endeavour had ROI of about 15 years 😂
(not considering that the process of learning is priceless 📖 📈 💸)


michondr.cz

a personal site on Strata template.

Before After
mobile michondr html mobile - before michondr vercel desktop - after
desktop michondr html desktop - before michondr vercel desktop - after

Images

Hugo itself helped tremendously as their image processing is 🤌. All the images are non-static, meaning they stay in /assets/images and are loaded through their resources.Get, resized and converted to webp.

This cannot be done for images referenced in markdown pages, but with a shortcode which loads the image as a resource. For example images in this article are called with this shortcode.

{< imagefll name="/images/speeding-up-my-sites/foo" type="png" alt="bar" >}}

Then loaded as a resource, resized, compressed and spit out as an image wrapped in a link to a full report image.

{{ $name := .Get "name" }}
{{ $type := .Get "type" }}
{{ $alt := .Get "alt" }}

{{ $small := print $name "." $type }}
{{ $large := print $name "_full." $type }}

{{ $small_image := resources.Get $small }}
{{ $large_image := resources.Get $large }}

{{ $small_image := $small_image.Crop "500x94 center" }}
{{ $large_image := $large_image.Resize "4000x webp q90" }}

<a target="_blank" href="{{ $large_image.RelPermalink }}">
    <img alt="{{ $alt }}" title="View full report" class="image fit" loading="lazy" src="{{ $small_image.RelPermalink }}">
</a>

Most of the remaining PageSpeed score was obtained by following the Diagnostics, such as adding titles/alt to links, images and icons.

CSS/JS

The template was overflowing with CSS which was mostly useless, so instead of cleaning it by hand I’ve gone with PurgeCSS which removed all the ballast.

What remained was about 20% of the size. Why load it with another request? I’ve embedded it inline to the head:

<head>
    ...
    
    {{ $css := resources.Get "/css/main.css" | resources.PostCSS }}
    
    {{ if hugo.IsProduction }}
        {{ $css = $css | minify | fingerprint | resources.PostProcess }}
    {{ end }}
    
    <style>{{ $css.Content | safeCSS }}</style>
</head>

Javascript got similar treatment, only they’re minified and loaded at the end of the body only after they’re needed.

{{ $main := resources.Get "js/main.js" | minify }}
<script src="{{ $main.RelPermalink }}" defer></script>

The template came with font-awesome which is just too slow and used only for icons. How wasteful. They Got replaced by SVG from TablerIcons

Layout shift

CLS was grinding my gears on mobile. The value was 0.88 which pulled my performance value to 65, very far from green.

After some digging, I discovered that skel is moving my footer - nicely on the desktop, but it moves the whole body up and down on mobile.

skel.on('+medium', function() {
    $footer.insertAfter($main);
});

skel.on('-medium !medium', function() {
    $footer.appendTo($header);
});

I went down the rabbit hole of debugging it, and this article by Chrome DevRel was very helpful.

This was solved simply by an if around the two event handlers to be active only on the desktop.

The last bit of layout shift was jumping due to fonts which load after a few milliseconds after FCP, so instead of

@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic&display=swap");

I just loaded the fonts in font-face. Just ctrl-c & ctrl-v what the link above gives.

@font-face {
    font-family: 'Source Sans Pro';
    font-style: italic;
    font-weight: 400;
    src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJT9g.woff2) format('woff2');
}
@font-face {
    font-family: 'Source Sans Pro';
    font-style: normal;
    font-weight: 400;
    src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2) format('woff2');
}

I might have gone a bit further by setting F-mods as this article shows but it is not necessary anymore.

Since I love dark mode, it felt like the right time to make the page dark. I inspired myself in the colors which DarkReader gives and tweaked some elements for core contrast so that Google is a bit happier.

And voila, the results:

Before - mobile Before - desktop After - mobile After - desktop
size 1.42MB 180kB (−87.3%)
LCP 6.6s 1.6s 0.8s (-88%) 0.5s (-69%)
speed index 4.4s 1.1s 0.8s (-82%) 0.2s (-82%)

doprcavky.cz

a crafty site on Axole template.

Before After
mobile doprcavky html mobile - before doprcavky vercel desktop - after
desktop doprcavky html desktop - before doprcavky vercel desktop - after

This was the same as above, only the Layout shift was not a big problem. I might push it a bit further by loading the font directly.

Most of the speed was gained on compressing the images which take up most of the site. And loading them as used scrolls down with loading="lazy".

This gave me about half the time to load:

Before - mobile Before - desktop After - mobile After - desktop
size 4.65Mb 880kB (-81%)
LCP 5.6s 1.2s 2.6s (-53%) 0.8s (-33%)
speed index 5.6s 1.2s 2.6s (-53%) 0.8s (-33%)