Images are where static sites usually lose their performance advantage. A single unoptimised hero photograph can cost more than every other asset combined.
Problem
I was resizing images by hand, which meant I stopped doing it. Pages shipped with 3 MB photographs and the Lighthouse score told the story.
My System
All image handling happens at build time, in three rules:
- One source file per image, stored in the content bundle or the assets directory at full resolution.
- Hugo generates the variants. Resize, WebP conversion and responsive widths come from the image pipeline, not from me.
- The template decides the sizes. A hero requests wide variants; a card requests small ones. Content authors never write a width.
{{ $src := resources.Get "img/example.jpg" }}
{{ $small := $src.Resize "640x webp" }}
{{ $large := $src.Resize "1600x webp" }}
<img src="{{ $large.RelPermalink }}"
srcset="{{ $small.RelPermalink }} 640w, {{ $large.RelPermalink }} 1600w"
sizes="(max-width: 720px) 100vw, 720px"
alt="Descriptive alt text" loading="lazy" decoding="async">
Rules I Do Not Break
- Alt text is written, never generated. If I cannot describe the image, it is decoration and gets an empty alt.
- Above-the-fold images load eagerly. Everything below is lazy.
- Width and height are always set, so layout never shifts while loading.
- SVG for diagrams and interface art, WebP or AVIF for photography.
Limitations
Build-time processing makes the first build slower on large archives. On a site with several hundred images, a full rebuild moves from seconds to minutes, which is tolerable for a weekly publish and annoying during design work.
Improvements
Moving the source images to object storage with on-demand transformation would remove the build cost entirely. That is the next experiment, and Lab Note 018 is where it is being tested.
