How We Rebuilt the Hege Website to Get a Perfect PageSpeed Score. Part 5: Optimize CSS

Welcome to the last piece of the performance puzzle, where we take a close look at how to optimize CSS like nobody’s business.

Steady, as She Goes

Welcome to the 5th part in our series on perfect website performance.

Here are all the articles in the series so far, if you want to check them out:

In this article, we’ll pick up where we left off in part 4, and turn to the topic of Cascading Style Sheets, also known as CSS. This is the language that controls how websites look — the visual layout, fonts, backgrounds, borders, colors, and so on. It’s a key part of any website, and it’s important to get it right.

So friends, without further ado, let’s CSS!

Hege, dressed in a blue jacket with a matching headband, is enjoying a bowl of noodles.

Do You Need to Optimize CSS? Yes.

In short, yes, you do need to pay attention to CSS. Bad CSS will make your page load slower, and it will make it more sluggish too. So optimizing CSS has a real impact on user experience.

Less CSS Means Your Page Will Render Faster

In the previous article, we compared the sizes of HTML and JavaScript between our old and new front pages. Let’s do the same with CSS, and see how they differ. The old page wasn’t horrible, but it was far from great. Partly because Webflow pulled in a lot of unnecessary things, partly because the page just wasn’t very well built from the start.

Here are the numbers relating to CSS:

Old front page New front page Improvement
Inline CSS 5.6 kB 5.1 kB
External CSS 30.4 kB 0
Total HTML + CSS 36.0 kB 5.1 kB 7.1x

(Sizes after gzipping with level 4 compression.)

The browser cannot start rendering the page until it has downloaded and parsed all the CSS. So less CSS means it can start rendering sooner. Partly because less CSS is faster to parse, but mostly because it takes less time to download. The network is the bottleneck.

As you can see above, the new CSS is 30.9 kB smaller, even after compression. On a slow mobile connection, that’s about 150 milliseconds (ms) saved. That might not sound like a lot, but remember that this is just one piece of the puzzle. To save that much time by just optimizing one thing is absolutely worthwhile.

Bad CSS Animations Can Hurt the User Experience

With good support for animations in CSS, these days there is little need to use JavaScript for animations.

However, you do need to take care to animate the right things. Using CSS for animations means you can get rid of JavaScript bloat, but in itself it doesn’t guarantee that your animations will run smooth.

Get animations wrong, and the user experience will suffer.

Level 1: Custom CSS, Preload, Animations

Okay fine, so we do need to consider CSS performance. But what to do?

Well, let’s start by getting the basics right.

Write Custom CSS

First off, don’t use frameworks. No Bootstrap. No Tailwind. No SCSS. No whatever. Just learn to write the damn CSS. Frameworks come and go. Learning the basics will take some time and effort up front, sure, but it’ll be worth it in the long run.

The entire CSS for our front page is less than 6 kilobytes. Six kilobytes. And it’s not even that well structured.

Preload Images and Fonts Referenced in External CSS

If your CSS loads from an external file (not inlined), make sure to preload any important images or font files that are referenced in it.

Why? Because otherwise the browser first has to download the CSS file, and only after it has done so can it start fetching these other files. This adds a whole TCP roundtrip.

On our front page, we inline both CSS and fonts, but on subpages we preload all the font files that will be referenced in the external CSS, like so:

<link rel="preload" href="/r/2812802085.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/r/600843795.woff2"  as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/r/915512105.woff2"  as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/r/3997950749.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/r/636367663.woff2"  as="font" type="font/woff2" crossorigin>

Animate the Right Things

If you use animations, make sure to animate the right CSS properties.

Just like a video game, you want your animations to run with as many frames per second (FPS) as possible. The higher the FPS, the smoother the animations. Now, and here’s the thing: this depends directly on how computationally expensive the animations are — some CSS properties take a lot of work for the browser to animate, while others take a lot less. So when designing your animations, you need to pick the right properties to animate.

Here’s the short of it:

  1. height is extremely expensive.
  2. Anything involving color is medium expensive
  3. transform and opacity are extremely cheap.

In other words, whenever you can, animate only transform and opacity.

This is not an article about how to do CSS animations. If you need somewhere to start out, here are two wonderful articles on the topic:

Level 2: Use Native CSS Features

In part 4 of the series, we mentioned how we use CSS scroll snapping to build the carousels on our front page. This isn’t a CSS optimization per se, but just like CSS animations, it’s a way of using CSS to cut down on JavaScript — when optimizing, you always have to think holistically.

We’ll quickly go through how we created our carousels, but the details aren’t super-important here — this isn’t a tutorial on scroll snapping. The point we want to make here is that this is one example of how you can use modern CSS features to simplify things a lot.

Example: Carousels With Scroll Snapping

On our front page, we have a memes section, where we show a series of our artworks in a carousel. On the phone, users can swipe left and right to scroll through the images in a natural way. On desktop, there are two buttons to move left and right.

Here’s how it looks:

Screenshot from hegecoin.com, showing 1) Hege escaping a crocodile riding an ostrich, and 2) Hege frightened by fireworks at a Chinese festival.

The HTML structure for this carousel looks something like this (simplified):

<ul id="memes-w" class="swipelist">
  <li>...</li>
  <li>...</li>
  <li>...</li>
</ul>
<ul class="back-fwd-ctrl">
  <li><button onclick="scrollSlider('memes-w', -1)">...</button></li>
  <li><button onclick="scrollSlider('memes-w', 1)">...</button></li>
</ul>

In CSS, we then set the scroll behavior to give a smooth swipe experience on mobile:

/* Horizontal swipe-able list */
.swipelist {
    display: flex;
    gap: 10px;
    align-items: stretch;
    overflow: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
}
.swipelist>* {
    scroll-snap-align: center;
    overflow: hidden;
}

And for desktop navigation, the buttons call this simple JavaScript function that changes the scroll position:

function scrollSlider(id, direction) {
  var e = document.getElementById(id);
  let w = e.firstElementChild.scrollWidth;
  e.scrollLeft += w * direction;
}

That’s a native swipe interface in <20 lines of CSS, 7 lines of JavaScript.

Level 3: Minify, Inline, Split

Now we move to the final touches.

When you have your CSS ready, there are a couple of things you can do to cut the size and deliver it as fast as possible.

Minify CSS

First, minify all CSS. This mostly removes all whitespace, which makes your files a little smaller. To be honest, it’s not going to make a huge difference. But it’s so easy it’s still worth doing.

Exactly how to do it depends on what tools you’re using. With Hugo, we can just add the minify filter:

{{ with resources.Get "css/style.css" | minify }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}

Inline CSS

Second, consider inlining CSS into the head of your HTML. If you inline all CSS, this will have a big impact, as it will save a whole TCP roundtrip, which could be as much as 150 ms on a slow connection.

Now, this is a tradeoff. Because if you inline CSS, that means it cannot be cached by the browser. So inlining will make the page display faster on the first visit, but might slow it down somewhat on subsequent visits.

You have to make a judgment call here. We inline the CSS on the front page, but not on subpages. We want first time visitors to get maximum speed as their first impression, but we also want to take advantage of caching on subpages.

Here is how you can inline CSS in Hugo:

{{ with resources.Get "css/style.css" | minify }}
<style>{{ .Content | safeCSS }}</style>
{{ end }}

Split CSS Depending on Page

Third and finally, don’t include a bunch of CSS that isn’t going to be used. This is especially important when inlining, as caching will not save you then.

We solved this by splitting our CSS in three parts:

  • base.css: Used on all pages.
  • home.css: Used only on the front page.
  • other.css: Used only on all other pages.

We then include the right combination depending on the page. Below is the full Hugo code we use in our template:

{{ $base := resources.ExecuteAsTemplate "css/base.css" . (resources.Get "css/base.css") }}
<!-- On the home page, load the home CSS, and inline it together with fonts -->
{{ if .IsHome }}
{{ $extra := resources.ExecuteAsTemplate "css/home.css" . (resources.Get "css/home.css") }}
{{ $css := slice $base $extra | resources.Concat "css/home-bundle.css" | minify }}
{{ $fonts := resources.ExecuteAsTemplate "css/home-fonts-inlined.css" . (resources.Get "css/home-fonts-inlined.css") | minify }}
<style>{{ replace $css.Content "\n" "" | safeCSS }}</style>
<style>{{ replace $fonts.Content "\n" "" | safeCSS }}</style>
<!-- On other pages, load the other CSS, not inlined -->
{{ else }}
{{ $extra := resources.ExecuteAsTemplate "css/other.css" . (resources.Get "css/other.css") }}
{{ $css := partial "r.html" (slice $base $extra | resources.Concat "css/other-bundle.css" | minify) }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">
{{ end }}

(We execute CSS files as Hugo templates so that we can use template logic in the CSS, too. And manually replacing newlines with replace got rid of some stray newlines that the minify filter for some reason didn’t remove.)

Conclusion: CSS Matters

And with that final optimization we have now covered all three levels that we went through. You’ve now seen why optimizing CSS matters for performance, and how we did it.

To recap our approach, first take care of the basics:

  • Write custom CSS.
  • Preload images and fonts as needed.
  • When possible, animate transform and opacity, not height.

Then consider:

  • Use native CSS features wherever applicable (like scroll snapping).

And finally:

  • Minify, inline, and split.

That’s it. All together, this shaved hundreds of milliseconds off the time to render, which was necessary to build a blazingly fast website (and reach that coveted perfect PageSpeed score).

The Next Step: Final Results

Optimizing CSS was the last piece in our performance puzzle. You’ve now seen everything we did to make hegecoin.com a top-tier performer, from building the solid base, to the technical tricks we pulled to squeeze out that last little bit of speed.

In the next and final article in the series we’ll make a recap of all the steps we took, show the final result, and leave you with some final thoughts around optimizing these kinds of things.

Until then, have a great week, and make sure to jump into our Telegram and say hi! Good vibes guaranteed.

Oh and by the way, if you want to check out the actual source code for this website, it’s all up on our GitHub.

— Team Hege ✌️

PS. Check out the piece “ROSE PULSE” below. This RARE collectible is up for sale on our DRiP!

This article was 100% written by human with 💛.

Hegend 1
Hegends NFTs Learn more about our NFT collection!