Hugo Image Optimization: Cutting an 8.6MB Background to 310KB

Using Hugo’s built-in image processing pipeline to compress an 8.6MB PNG background to a 310KB WebP — a 28x size reduction, cutting slow-network load time from 5-10s to under 1s.

1. Why Optimize Images

Images are usually the biggest culprit for slow static sites:

ReasonExplanation
Large file sizeA 2560×1440 PNG can be 8-10MB
Inefficient formatPNG is lossless — great for screenshots, bad for photos
No lazy loadingPage loads all images at once
No cachingRe-downloads on every visit

Core idea: make the browser download smaller files.

2. Hugo Image Processing Pipeline

Hugo has built-in image processing that runs at build time:

$$Original image (assets/img/wall4.png) ↓ processed at build Resize "1920x webp q75" ↓ Optimized image (public/img/wall4_hu_xxx.webp)$$
OperationSyntaxDescription
ResizeResize "800x"Limit width to 800px
CropCrop "800x600"Crop to exact size
FillFill "800x600"Fill and center-crop
Convertwebp / jpg / pngWebP is smallest
Qualityq7575% quality (default 75)

3. CSS Background Image Optimization

Step 1: Put the image in Hugo’s assets dir

mkdir -p web-site/assets/img
cp your-image.png web-site/assets/img/wall4.png

Step 2: Create a processing partial

layouts/partials/hero-bg.html:

{{ $img := resources.Get "img/wall4.png" }}
{{ $webp := "" }}
{{ if $img }}
  {{ $webp = $img.Resize "1920x webp q75" }}
{{ end }}
{{ if $webp }}
<style>
  :root {
    --tn-hero-bg: url('{{ $webp.RelPermalink }}');
  }
</style>
{{ end }}

Step 3: Inject in page templates

Add one line to the <head> of every baseof template:

<head>
  {{ partial "head.html" . }}
  {{ partial "hero-bg.html" . }}    <!-- ← add this -->
</head>

Step 4: Reference the variable in CSS

.tn-hero {
  background:
    linear-gradient(rgba(18, 32, 88, 0.75), rgba(18, 32, 88, 0.85)),
    var(--tn-hero-bg, url('/wall4.png')) center center / cover no-repeat;
}

Step 5: Rebuild

hugo
find public -name "*.webp"
# → public/img/wall4_hu_8de1eac147519903.webp

4. Inline Images (shortcode)

Plain ![](img.jpg) markdown does NOT go through Hugo’s pipeline. Create a shortcode:

layouts/shortcodes/img.html:

{{ $img := .Page.Resources.GetMatch (.Get "src") }}
{{ $alt := .Get "alt" | default "" }}
{{ $width := .Get "width" | default "800x" }}

{{ if $img }}
  {{ $webp := $img.Resize (printf "%s webp q75" $width) }}
  <figure>
    <img src="{{ $webp.RelPermalink }}"
         alt="{{ $alt }}"
         loading="lazy"
         width="{{ $webp.Width }}" height="{{ $webp.Height }}" />
    {{ with .Get "caption" }}<figcaption>{{ . }}</figcaption>{{ end }}
  </figure>
{{ end }}

Usage:

{{< img src="photo.jpg" alt="Product" caption="Fig 1: Product" >}}

Note: images must live next to the post (Page Bundle):

$$content/blog/my-post/ ├── index.md ← the post (index.md, not xxx.md) └── photo.jpg ← image beside the post$$

5. Lazy Loading

Add one attribute to any <img> — the browser loads it only when scrolled into view:

<img src="photo.webp" loading="lazy" />

6. Measuring Results

  1. Press F12
  2. Go to Network tab
  3. Set throttling to Regular 3G or Slow 3G
  4. Check Disable cache
  5. Ctrl+Shift+R hard reload
  6. Look at the Time column for the image

curl

curl -o /dev/null -w "time: %{time_total}s  size: %{size_download} bytes\n" \
  http://localhost:1313/img/wall4_hu_8de1eac147519903.webp

Measured results

VersionSizeRegular 3G time
PNG 2560×14408.6 MB5-10 s
WebP 1920px q75310 KB<1 s

28x smaller; slow-network load dropped from 5-10s to under 1s.

7. Advanced Tips

7.1 Responsive images (srcset)

{{ $img := resources.Get "img/hero.png" }}
{{ $sm := $img.Resize "640x webp q75" }}
{{ $md := $img.Resize "1280x webp q75" }}
{{ $lg := $img.Resize "1920x webp q75" }}

<img src="{{ $lg.RelPermalink }}"
     srcset="{{ $sm.RelPermalink }} 640w, {{ $md.RelPermalink }} 1280w, {{ $lg.RelPermalink }} 1920w"
     sizes="100vw"
     loading="lazy" />

7.2 More aggressive compression

$img.Resize "1920x webp q50"   // 50% quality
$img.Resize "1920x webp q30"   // 30% quality, fine for flat backgrounds

7.3 Nginx caching

location ~* \.(jpg|jpeg|png|webp|gif|svg)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

7.4 CDN

  • Cloudflare: free CDN in front of your site, auto-cache + compress
  • jsDelivr: free CDN for GitHub/Gitee static assets

Summary

$$1. Put images in assets/ 2. Use resources.Get + Resize "xxx webp q75" 3. CSS background → inject CSS variable 4. Inline images → custom shortcode 5. Add loading="lazy" 6. Verify with browser DevTools$$