Slimming down your Hugo static site


posted | about 9 minutes to read

tags: hugo web development postcss

Over the last few months, I have spent a good chunk of time working on minimizing the page load on my website. When I first redeveloped out of Wordpress. Of course, the previous site was massive; I think I was running somewhere around a 2 MB page load with all the scripts and high-resolution pictures and stuff. One of my goals with the redevelopment into Hugo was to shrink that down, but I couldn’t do it all at once - I had to take a few steps to get to where I am now, and the process certainly was not without its hiccups (my favorite was the time I uploaded a 30 MB JPEG to one of my posts and didn’t resize it at all ever). Nevertheless, I think I have probably gotten things to the best place they can be at this point while still allowing me to work in a way in which I am comfortable. In this post I’d like to go over the steps I took to get to where I am today.

Image resizing in Hugo

A few months after I initially switched over to my Hugo site, I finally started to look at CloudFront a little bit closer to understand how much bandwidth I was chewing and what files were causing it1. There were a few things that stood out, but by far the biggest offenders were my images. The way I published my posts at the time, I was just embedding the images right in the posts - no thumbnailing or resizing or anything. This meant that a post with pictures was taking up multiple megabytes of bandwidth on every page load. This was obviously not something I wanted to have happening, and I figured this would be some low hanging fruit. In fact, I was correct about this; Hugo has some built-in image processing tools, including an image resizer. The real trick, though, was that I wanted to keep the full-resolution images accessible in some way - resizing every picture on my site to 200x150 would make for a really rough experience especially for pictures with fine detail2. Fortunately, I found a solution that I could adapt to my needs - Laura Kalbag wrote an excellent image processing script which I wa able to adapt to what I needed. Using Hugo Page Bundles to add images to pages as resources worked really well; the major change I made was that I just defaulted every image on the actual page to a very small image, and then linked through to the full size image. I also added a bunch more attributes to my img shortcode, as follows:

  {{ if .Get "title" }}
    {{ with .Get "title" }}
      title="{{.}}"
    {{ end }}
  {{ else }}
    {{ if .Get "alt" }}
      {{with .Get "alt" }}
        title="{{.}}"
      {{ end }}
    {{ end }}
   {{ end }}
  {{ if .Get "class" }}
    {{ with .Get "class" }}
      class="{{.}}"
    {{ end }}
  {{ end }}
  {{ if .Get "id" }}
    {{ with .Get "id" }}
      id="{{.}}"
    {{ end }}
  {{ end }}
  {{ if .Get "height" }}
    {{ with .Get "height" }}
      height="{{.}}"
    {{ end }}
  {{ end }}
  {{ if .Get "width" }}
    {{ with .Get "width" }}
      width="{{.}}"
    {{ end }}
  {{ end }}
  {{ if .Get "hspace" }}
    {{ with .Get "hspace" }}
      hspace="{{.}}"
    {{ end }}
  {{ end }}
  {{ if .Get "longdesc" }}
    {{ $res := .Page.Resources.GetMatch (.Get "longdesc") }}
    longdesc="{{ $res.RelPermalink }}"
  {{ end }}

There are probably way more efficient ways of doing this, but the idea was basically to duplicate a bunch of different properties from the shortcode into the eventual image tag. The longdesc one is the only one that really stands out as unique; for that, I set up a resource in the page bundle to point to a text file with the same name as the image to which it relates. I don’t use this often, but occasionally it’s necessary (especially for collages of pictures).

After implementing this shortcode, I saw my page size - and, more importantly, my page load times - drop dramatically - 50% or more, on pages with images. I still had a lot of work to do, though.

Removing Webfonts

For quite some time, I was serving a couple of Google fonts - Lora and Raleway - as part of every page load. I was doing this for design reasons, but eventually I decided that the bandwidth wasn’t worth it and that browser fonts were fine anyway, so I just dropped them out of my CSS and stopped loading them. Saved me another two requests and a good chunk of bandwidth.

Similarly, I was using Font Awesome - which is a great webfont icon package - for icons on my website. Removing it and replacing links with words, I felt, improved accessibility and didn’t really hurt the design (take a look at the footer as it looks today, for example). This was another pretty easy fix - just a quick couple of changes to the theme - and saved me megabytes per day in bandwidth.

Minify HTML

This one is very silly and very easy. I just started building my Hugo site with hugo --minify instead of just hugo and it removed all the superfluous spacing. No issues at all, and it’s the easiest thing in the world to add it into your build process - just modifying the one command.

Removing jQuery

I often use jQuery in my projects because I know how to use it and I’m comfortable with it3. jQuery is 29 KB when gzipped and minified, which should tell you how far down the rabbit hole we are here - in the tens of kilobytes now, instead of in the megabytes that I was dealing with before. Still, this, alongside my CSS, was the biggest driver of bandwidth at the time I started looking at it, so it was the next to go - which ended up being a bit more of a lift. Fortunately I wasn’t using any of the Javascript features of Bootstrap, so I didn’t have to worry about that - but all of the JavaScript on my website was heavily jQuerified, so this meant rewriting my comments system and contact form in pure JavaScript, and then a chunk of testing to make sure I didn’t screw anything up.

Minify CSS

I have, from the very start of this site, minified and bundled my CSS together using Hugo builtins. My first iteration looked something like this:

{{ $bootstrap := resources.Get "css/bootstrap.min.css" }}
{{ $overrides := resources.Get "css/overrides.css" | resources.Minify }}
{{ $css := slice $bootstrap $overrides | resources.Concat "bundle.min.css" }}

It worked fine, but I was still packaging the entirety of Bootstrap when I was only really using the basic styling, and that didn’t make sense. Bootstrap is not a small CSS framework as such things are measured, and given how little of it I was using, it really didn’t make sense for me to be serving the whole darn thing - but I certainly wasn’t going to go in and cut out all the stuff I wasn’t using, especially if I might end up using the styling in the future.

I had been looking into ways to do this using Hugo Pipes for some time, and it just seemed really complicated to me. Finally I just decided to bite the bullet and go for it, and set up PostCSS and its purgecss plugin as part of my build pipeline. I used a Hugo forum thread as my jumping off point, and the implementation actualy ended up being pretty easy. All I had to do was install node and npm, build a config.json, and run npm install - and after adding the appropriate files to my Hugo theme, it worked perfectly. The only major gotcha I ran into is that I had to make sure that every CSS class that I was loading in dynamically or directly referencing in a post was listed in my purgecss whitelist in postcss.config.js:

module.exports = {
    plugins: {
      '@fullhuman/postcss-purgecss': {
        content: ['themes/ajl-io/layouts/**/*.html'],
        whitelist: [
          'card',
          'card-body',
          'mb-1',
          'bg-light',
          'comment-author',
          'comment-timestamp',
          'characters-remaining',
          'form-horizontal',
          'form-group',
          'control-label',
          'form-control',
          'btn',
          'btn-success',
          'rounded'
        ]
      },
      autoprefixer: {},
      cssnano: { preset: 'default' }
    }
  };

Most of these are styling from the comment system - and without this whitelist, none of them would have worked in the published site. Once this was in place, I updated my CSS building code:

{{ $bootstrap := resources.Get "css/bootstrap.min.css" }}
{{ $overrides := resources.Get "css/overrides.css" }}
{{ $css := slice $bootstrap $overrides | resources.Concat "bundle.css" | postCSS | minify }}

The general idea is that I’d concatenate everything together, then run all the PostCSS stuff to deduplicate rules and take out everything I didn’t need - and it worked! All told, I think I dropped about 90% of the CSS on my site - just a bunch of stuff that wasn’t being used at all. Plus, the original Bootstrap CSS file is still present in the theme - so if I do end up using more Bootstrap features later, PostCSS will automatically update my CSS to include those features.

Wrapping up

There’s a lot in here that doesn’t seem like it would make a huge difference, right? Like, what’s 20 KB here or there in the grand scheme of things? The way I look at it, though, this really starts mattering when you’re operating at scale. 20 KB savings, spread across thousands or millions of page loads, can really start adding up, and so I think for busy websites and especially for situations when you’re paying for your bandwidth, it’s critical to cut every corner you can4. All of the changes above ended up cutting my bandwidth by about 70% since launching my website. For me, that’s still a cost measured in cents per month, but for a heavy-traffic site leveraging CloudFront, it could be a much bigger difference. Probably most important, though, is the user experience; by cutting requests way down and decreasing page size, you make your content more accessible across the spectrum of Internet users, from folks on gigabit fiber all the way down to the people still using dial-up. I would say that my site is proof that it’s possible to do this without compromising readability or design philosophy.


  1. The Popular Objects Report, located within the CloudFront console, is super useful for this - you can sort by total bytes served, which I found very helpful. ↩︎

  2. One of these days I’d like to post a highlight reel of some of my favorite photos I’ve taken, but that’s a project that’s on the back burner for now. ↩︎

  3. A lot of this for me was driven by Bootstrap using it; Bootstrap is the only CSS framework I actually know how to use (yes, I know, I’m awful) and that packages jQuery so it was a natural extension for me to just write everything in jQuery. ↩︎

  4. Within reason. For example, I’m not cutting images completely off my website, but I’m making informed decisions about how I use them and how they are displayed on the site. ↩︎