Image optimization in Hugo
Go through the easy process of optimizing images in Hugo to create <img> elements that represent good practice. It will cover recommended attributes, image compression, the automatic generation of alternative formats, and cache busting using fingerprint.
7 minutes
Tip using Ko-fi or Buy Me a Coffee
I will go through the easy process of optimizing images in Hugo to create <img>
elements that represent good practice. It will cover recommended attributes, image compression, the automatic generation of alternative formats, and cache busting using fingerprint.
The basics
Suppose I want to insert a new image header. In this case, the most straightforward approach is to copy an image to the static
directory and directly reference it in a partial or default template.
For example, a header partial template at: themes/mytheme/layouts/partials/header.html
.
While the banner.jpg
image would be located at: static/banner.jpg
.
# an example command to copy banner.jpg to the static directory
cp banner.jpg /mysite/static
1<header>
2 <img src="/banner.jpg">
3</header>
Including the leading /
forward slash is essential to tell the browser that the image is accessible from the server’s root.
While this is the most straightforward approach does not allow Hugo to interact with the image. So I personally never recommend using the static
directories for any custom content. Instead, use the theme assets
directory to hold any site images unrelated to individual pages and posts.
# an example command to move banner.jpg into the assets directory
cd mysite
mv static/banner.jpg themes/mytheme/assets/images/banner.jpg
We can use the Hugo resources.Get
function to retrieve the image and store it as a variable.
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="/banner.jpg">
Update the src
attribute to use the $banner
variable as a relative path to the image.
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $$banner.RelPermalink }}">
It will generate the following HTML.
<img src="/images/banner.jpg">
Or use Permalink
for an absolute path to the image.
1<img src="{{ $banner.Permalink }}">
<img src="http://localhost:1313/images/banner.jpg">
Alt attributes
It’s essential for accessibility to always include descriptive image texts with the alt
attribute.
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" alt="The skyline of Sydney, Australia">
Width and height dimensions
You should also include width and height dimensions for each image.
Always include width and height size attributes on your images... This approach ensures that the browser can allocate the correct amount of space in the document while the image is loading
# obtaining the dimensions in Linux
$ exiv2 banner.jpg
Image size : 2989 x 812
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" width="2989" height="812" alt="The skyline of Sydney, Australia">
Hugo can generate these values for you with the Width
and Height
properties.
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
Lazy loading
As of 2022, the <img loading="">
attribute is recent and should be in use on all <img>
elements. The loading
supports two values, eager
and lazy
.
eager
loads the image immediately and is the default, traditional browser behavior.
lazy
defers the loading of the image until the user scrolls to it, improving browser performance and saving on bandwidth.
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It's a way to shorten the length of the critical rendering path, which translates into reduced page load times
Optimize images
Hugo has some great Image Processing tools for optimizing your images. Unfortunately, it is not always obvious how to do some simple tasks, so I unintuitively use the Resize method to optimize images but keep their dimensions.
Create a new $jpeg
variable which will run the Resize
method to optimize the banner.jpg
.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize "2989x812 jpeg" }}
While keeping the original dimensions, I see considerable file size savings with the resized JPEG image.
# printing the file size of the original banner.jpg
$ du -h banner.jpg
1.1M
# printing the file size of the Hugo optimized banner.jpg
$ du -h _huf9c78e74655fc44f9887c1375f5eec72_1142285_5ba59f127937afa35edcf83637b4809b.jpg
336K
Update the <img src="">
attribute to point to the optimized image $jpeg
.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize "2989x812 jpeg" }}
3<img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
Finally, update the $jpeg
variable to use Hugo’s Width
and Height
values, so we don’t have to worry about ever-changing these in the future.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
printf
is a useful but not well explained Go function.
It lets you combine different variables values together as a formatted string. %d
is a placeholder for a number value (in Go, it’s referenced as base 10 integer).
printf "%dx%d jpeg" 0 0
would return "0x0 jpeg"
printf "%dx%d jpeg" 1000 500
would return "1000x500 jpeg"
printf "%dx%d jpeg" $banner.Width $banner.Height
would return "2989x812 jpeg"
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3<img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
Alternative image formats
Another way to optimize page loads is to offer alternative image formats. The JPEG banner is a universally viewable format. However, newer optimized formats exist that we can also quickly provide, thanks to Hugo and HTML5. By serving a WebP (Web Picture format) supplement, there’s a potential 25% download savings.
Create a new $webp
variable to generate the new image format, which follows the same syntax as $jpeg
.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}
There’s an additional option in the $webp
variable, photo
. This is known as a WebP hint, and it’s only applicable to WebP and corresponds to a set of pre-defined encoding parameters.
# printing the file size of the Hugo optimized banner as a webp image.
# as a JPEG it is 336K.
$ du -h banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_h2_box.webp
244K
To use the WebP image in our HTML, we introduce the <picture>
and <source>
elements.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}
4<picture>
5 <source srcset="{{ $webp.RelPermalink }}" type="image/webp">
6 <img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
7</picture>
The picture element offers alternative versions of an image for different scenarios. It will be up to the browser to determine the best image to use. A <picture>
can only ever have one <img>
element but can have multiple <source>
. The image attributes such as loading
, alt
, and width
/height
can only be used with the <img>
element.
The <source srcset="">
attribute contains one or more links to images, and the type
is the MIME media type.
Note multiple source
elements should be ordered in display preference, while the img
element should link to the most compatible fallback image.
1<picture>
2 <source srcset="/images/banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_h2_box.webp" type="image/webp">
3 <img src="/images/banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_box.jpg" loading="lazy" width="2989" height="812"
4 alt="The skyline of Sydney, Australia">
5</picture>

Caching busting
Caching is critical to a website’s performance in the browser and online using Content Delivery Networks. Caching stores and uses a copy of the file locally instead of always fetching it from your remote web server. It is great until you need to update the file and various caches serve the older, replaced file.
The easiest way to avoid “stale caches” is to give the file a unique filename that gets changed anytime the file is updated. Again, thankfully Hugo makes this easy using its fingerprint
pipe.
Please note that the Resize
method and other Hugo processed images do not require fingerprinting.
1{{- $banner := resources.Get "images/banner.jpg" | fingerprint }}
2<img src="{{ $banner.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
<img src="/images/banner.3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0.jpg" loading="lazy" width="2989" height="812" alt="The skyline of Sydney, Australia">
If I ever modify or replace banner.jpg
, the partial filename will differ. It is the SHA256 checksum of the file.
banner.3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0.jpg
$ shasum --algorithm=256 banner.jpg
3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0 banner.jpg
Practical example
Putting it together in a new, generic Hugo theme template. This code sample is a mock up of a header, partial template: themes/mytheme/layouts/partials/header.html
.
The banner.jpg
image would be located at: themes/mytheme/assets/images/banner.jpg
.
1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}
4<header>
5 <h1>{{ .Site.Title }}</h1>
6 <div class="banner">
7 <picture>
8 <source srcset="{{ $webp.RelPermalink }}" type="image/webp">
9 <img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
10 </picture>
11 </div>
12</header>
Written by Ben Garrett