loading...
Cover image for Lazy Loading Images in Hugo

Lazy Loading Images in Hugo

p4lm profile image Henrik Sommerfeld Originally published at henriksommerfeld.se ・4 min read

When writing another post, I realised that I hadn't documented/described my image lazy loading implementation in Hugo anywhere, so here it comes. Full source code is in this repo.

The first thing we need are responsive images, not the thing you get when setting the width to 100% in CSS, but different versions of the same image in different resolutions so that the web browser can pick the best one (using srcset).

Changing folder structure

The structure I had before implementing this was all the Markdown files in the same folder with the images in a common static/images folder. To use image processing in Hugo we need to use Page Resources, meaning we have to create a folder for each post where we put both the Markdown file and the images related to that post.

Before:

content
 - articles
   - 2019-11-15-dark-mode.md
   - 2019-11-17-netlify-cms.md

After:

content
 - articles
   - 2019-11-15-dark-mode
     - index.md
     - jay-wennington-loAgTdeDcIU-unsplash.jpg
   - 2019-11-17-netlify-cms
     - hero-image.png
     - index.md
     - insert-image.png

Writing a shortcode

To be able to reference the images in the Markdown files, we need a shortcode. This is the trickiest part to explain since the code is a bit lengthy. This is because I have the possibility to pass parameters for whether or not to have a border, a custom width, a lightbox and caption. It might also be because my Go template skills are questionable.

I use the shortcode in my index.md file to reference the image like this:

{{<post-image image="hero-image.png" />}}

What happens in the shortcode implementation below is that I create a blurred version of the image resized to 48px wide (keeping the aspect ratio). This is the image you will see before a better version is fetched. This small image is embedded into the HTML as a Base64 encoded string, so that there is no extra request required to get the initial image.

A few different versions are then generated, but only if the original image is larger than the version I'm trying to generate. I don't want to upscale an image (it will be blurry). The variable $src_set is appended with each version and will contain all versions (sizes) when the HTML part begins.

Shortcode post-image.html:

{{ $image := (.Page.Resources.GetMatch  (index .Params.image)) }}
{{ $alt := .Get "alt" }}
{{ $width := .Get "width" }}
{{ $borderless := .Get "borderless" }}
{{ $placeholder := ($image.Resize "48x q20") | images.Filter (images.GaussianBlur 6) }}
{{ $src := $image }}
{{ $src_set := ""}}

{{ $src_set = (print $image.RelPermalink " " $image.Width "w") }}
{{ $src := $image }}

{{ if ge $image.Width "500"}}
{{ $x_small := $image.Resize "500x" }}
{{ $src_set = (print $src_set ", "  $x_small.RelPermalink " 500w") }}
{{ end }}

{{ if ge $image.Width "800"}}
{{ $small := $image.Resize "800x" }}
{{ $src_set = (print $src_set ", " $small.RelPermalink " 800w") }}
{{ end }}

{{ if ge $image.Width "1200"}}
{{ $medium := $image.Resize "1200x" }}
{{ $src_set = (print $src_set ", " $medium.RelPermalink " 1200w") }}
{{ end }}

{{ if gt $image.Width "1500"}}
{{ $large := $image.Resize "1500x" }}
{{ $src_set = (print $src_set ", " $large.RelPermalink " 1500w") }}
{{ end }}

{{ $border_class := "image-border" }}
{{ if $borderless}}
{{ $border_class = "" }}
{{ end }}


<noscript>
  <style>
    figure.lazy {
      display: none;
    }
  </style>
  <figure class="{{ $border_class }}">
    {{ if .Get "lightbox" }}
    <a href='{{ $image.RelPermalink }}'>
      {{ end }}
      <img src="{{ $src.RelPermalink }}" {{ if $width }}width="{{$width}}"{{ end }} />
      {{ if .Get "lightbox" }}
    </a>
    {{ end }}
    <figcaption>
      <em>{{ .Inner }}</em>
    </figcaption>
  </figure>
</noscript>

<figure class="{{ $border_class }} lazy">
  {{ if .Get "lightbox" }}
  <a href='{{ $image.RelPermalink }}'>
    {{ end }}
    <img class="lazyload" data-sizes="auto" src="{{ $src.RelPermalink }}" {{ if $width }}width="{{$width}}"{{ end }}
      srcset="data:image/jpeg;base64,{{ $placeholder.Content | base64Encode }}" data-src="{{ $src.RelPermalink }}"
      data-srcset="{{ $src_set }}" width="{{ $image.Width }}" height="{{ $image.Height }}" alt="{{ $alt }}" />
    {{ if .Get "lightbox" }}
  </a>
  {{ end }}
  {{ if .Inner }}
  <figcaption>
    <em>{{ .Inner }}</em>
  </figcaption>
  {{ end }}
</figure>

Adding Javascript

There are a few CSS classes set here that acts as a signal to different Javascript features. lightbox is such a thing, but the interesting one here is lazyload. I use a Javascript library called lazysizes that is included in my Javascript bundle and I have created the HTML to work with that library.

You might have noticed that the code above looks somewhat duplicated, that's because I have a noscript tag for those with Javascript disabled. In that case, a srcset tag is still used to provide the best image, but without the lazy loading.

There is no "blur up effect" here. That's a conscious decision, since this is a bit simpler to implement and feels faster, in my opinion.

Performance consideration

In Hugo's documentation for image processing it's clearly stated that it's recommended to include the generated images in source control. This is to avoid generating the same images over and over again. On a large site with lots of images, this can make a big difference.

Discussion

pic
Editor guide