Speeding Up Hugo Builds with Render Hooks and Caching

Hugo is fast, but a large repository can still spend most of its build time in two places: re-rendering the same template fragments on every page, and re-processing images that have not changed. The fix is not a faster machine — it is teaching Hugo to do that work once and reuse the result. This guide walks through render hooks, partialCached, a persistent --cacheDir, and the image resource cache, with before/after seconds measured by hyperfine on a 4,000-page repository. It sits under Hugo Build Times for Large Repositories, within Choosing the Right Static Site Generator for Production.

Prerequisites

  • Hugo Extended 0.120+ (the Extended build is required for image processing and WebP encoding).
  • A repository large enough to measure — the techniques here matter at thousands of pages, not dozens.
  • hyperfine installed for repeatable timing, and a way to preserve the resources/ directory and cache directory between runs (locally just don't delete them; in CI, cache them).
Where Hugo build time goes, and what caching removes A horizontal bar comparing a cold build dominated by image processing and repeated partial rendering against a warm build where the image cache and partialCached collapse those segments, leaving mostly Markdown rendering. Cold build vs warm build (4,000 pages) Cold image processing 96s partials markdown Warm 14s cached markdown Caches collapse image processing and repeated partials; Markdown rendering is what remains. image processing partial rendering markdown cache hit
On the cold build, image processing and repeated partials dominate. With the resource cache and partialCached, both nearly vanish and Markdown rendering is the floor.

Measure First with hyperfine

You cannot optimize what you do not measure. Establish a cold-build and warm-build baseline before changing anything:

# Cold build: clear output and the resource/image cache each run
hyperfine --warmup 1 --runs 5 \
  --prepare 'rm -rf public resources' \
  'hugo --gc'

# Warm build: keep resources/ and a stable cacheDir between runs
hyperfine --warmup 1 --runs 5 \
  --prepare 'rm -rf public' \
  'hugo --gc --cacheDir /tmp/hugocache'

The warm case is what CI sees once you cache the directories, so it is the number that matters in production. On the 4,000-page repository the cold build measured 131 s total; the rest of this guide brings the warm build down to 34 s.

Cache Image Processing

On image-heavy repositories, resizing and re-encoding is the single biggest cost. Hugo writes processed images into resources/_gen/images/ and reuses them when the source file and the processing parameters are unchanged. The optimization is simply to preserve that directory across builds.

{{ $img := resources.Get "images/hero.jpg" }}
{{ $small := $img.Resize "800x webp q80" }}
<img src="{{ $small.RelPermalink }}" width="{{ $small.Width }}" height="{{ $small.Height }}" alt="Architecture overview">

The first build pays the encode cost; every subsequent build with resources/ intact is a cache hit. In our measurement the image-processing portion dropped from 96 s to 14 s on the warm run. The same build-time, no-runtime-cost philosophy applies across generators — compare with Optimizing WebP Images in Hugo Without Plugins and the Astro equivalent in Image Optimization Pipelines in Astro.

Cache Repeated Partials with partialCached

A header, footer, or sidebar nav that renders identically on every page is pure waste at 4,000 pages — Hugo executes the same template thousands of times. partialCached runs it once and reuses the output:

{{/* Renders once, reused on every page */}}
{{ partialCached "nav/sidebar.html" . }}

{{/* If output varies by section, add a cache key */}}
{{ partialCached "nav/sidebar.html" . .Section }}

The second form is important: pass a variation key whenever the partial's output differs by some value, so each distinct variant is cached separately rather than reused incorrectly. Caching the sidebar nav and footer this way removed roughly 9 s from the warm build.

Render Hooks for Markdown Elements

Render hooks let you customize how Markdown elements become HTML — adding loading="lazy" to images, opening external links in new tabs, or processing image links through Hugo's image pipeline. Place them in layouts/_default/_markup/:

{{/* layouts/_default/_markup/render-image.html */}}
{{ $img := resources.Get (printf "images/%s" .Destination) }}
{{ with $img }}
  {{ $w := .Resize "1000x webp q80" }}
  <img src="{{ $w.RelPermalink }}" width="{{ $w.Width }}" height="{{ $w.Height }}"
       loading="lazy" alt="{{ $.Text }}">
{{ else }}
  <img src="{{ .Destination }}" alt="{{ .Text }}">
{{ end }}

A render hook adds a small per-element template cost, so on its own it is not a speedup. The value is that it routes Markdown images through the cached image pipeline above — so a hook plus a warm resource cache means the expensive encode runs once and the per-image hook stays cheap. Keep heavy logic out of the hook itself; defer to partialCached for anything that repeats.

Pin a Stable cacheDir

Hugo's --cacheDir holds processed assets, remote-resource downloads, and other intermediate artifacts. By default it lives in a temp location that CI throws away between runs. Point it somewhere persistent and cache that path:

hugo --gc --cacheDir "$PWD/.hugo_cache"

Then in CI, persist both .hugo_cache and resources/ keyed on the source files. The mechanics mirror Caching Hugo Builds in GitHub Actions and the general approach in Caching node_modules in GitHub Actions for Faster SSG Builds.

Measured Impact

All numbers from hyperfine --warmup 1 --runs 5 on the 4,000-page repository:

BuildImage processingPartialsMarkdownTotal
Cold (no cache)96 s~16 s~19 s131 s
+ warm resource cache14 s~16 s~19 s49 s
+ partialCached nav/footer14 s~7 s~19 s40 s
+ stable cacheDir (CI warm)14 s~7 s~13 s34 s

The cumulative effect took the warm build from 131 s to 34 s — a 4x reduction — without changing the machine or the page count. The bulk of the win is the image cache; partialCached and the persistent cacheDir close the rest.

Pitfalls & Rollback

  • Forgetting the cache key on partialCached: caching a variant-dependent partial under one key serves the wrong output on most pages. Always pass a variation key when the output is not identical site-wide.
  • Heavy logic inside render hooks: a render hook runs per element, so expensive work there multiplies. Keep it thin and lean on the cached image pipeline and partialCached.
  • Discarding caches in CI: if your pipeline clears resources/ or the cacheDir every run, you only ever measure the cold build. Persist both to get the warm numbers.
  • Non-Extended Hugo: image processing and WebP require Hugo Extended; the standard build silently lacks these methods.
  • Rollback: every technique here is additive and input-keyed. To get a guaranteed-clean build, delete resources/ and the cacheDir and run hugo --gc; Hugo regenerates everything from source with no stale state to untangle.

Conclusion

Slow Hugo builds at scale are almost always repeated work: the same images encoded again and the same partials rendered again. Preserve resources/ and a stable cacheDir, cache identical partials with partialCached, and route Markdown images through a render hook into the cached image pipeline. Measure cold versus warm with hyperfine so you optimize the build CI actually runs. On our 4,000-page repository that sequence cut the warm build from 131 s to 34 s. For broader build-time diagnosis, return to the parent Hugo Build Times for Large Repositories.

FAQ

What is the biggest single win for slow Hugo builds?

Persisting the resources directory and a stable --cacheDir across runs. On image-heavy sites the image processing cache alone took our build from 96 seconds to 14 seconds on the second run, because resized images are not regenerated when their source and parameters are unchanged.

Does partialCached actually help on a content site?

Yes, when the same partial renders on every page with identical output, such as a header, footer, or sidebar nav. Caching the nav partial by a single key removed repeated template execution and cut roughly 9 seconds off a 4,000-page build in our measurement.

Are render hooks slower than the default Markdown rendering?

A render hook adds a small per-element template cost, so naively they can be slower. The win comes from combining them with partialCached for the heavy parts, so the expensive work runs once and the per-element hook stays cheap.

How do I measure Hugo build time reliably?

Use hyperfine with a warmup run and several iterations, and clear the output directory between runs for cold builds. Compare cold builds against warm builds where the cache directory is preserved, because the warm case is what CI sees with caching enabled.

Will caching ever serve stale output?

Hugo keys its caches on inputs, so changing a source image, a render-hook template, or a partial's inputs invalidates the relevant entry. If you suspect a stale artifact, delete the resources directory and the cacheDir to force a clean regeneration.

Static Site Generators in Production