Hugo Build Times for Large Repositories

Hugo is the fastest mainstream static site generator, but at tens of thousands of pages even Hugo's build time becomes something you have to manage deliberately. This guide covers diagnosing where the time goes, the image-processing and render-hook costs that dominate large repos, the caching that actually helps in CI, and the template patterns that cause super-linear blowups. It assumes the broader framework decision is settled — if not, start with Choosing the Right Static Site Generator for Production.

A note on expectations up front: Hugo rebuilds the whole site on each hugo run — it has no incremental production build. (Only hugo server does fast in-memory partial rebuilds during local development.) So "speeding up builds" means making the full build cheaper and avoiding redundant work in CI, not skipping pages.

Where time goes in a large Hugo build, before and after tuning A pipeline showing content parsing, template rendering, image processing, and render hooks, each labelled with a share of a 42-second cold build, alongside an after-tuning total of 16 seconds measured with hyperfine. A 10k-page Hugo build: where the seconds go Parse content frontmatter + Markdown ~6s Render templates & partials ~14s Process images Resize / Fill encode WebP ~16s Render hooks links / images per element ~6s Before tuning cold full build, hyperfine mean 42.1s After tuning 16.4s
On a 10k-page repo, image processing and template rendering dominate the cold build; caching processed resources and scoping iterations cut a 42.1s build to 16.4s (hyperfine mean of 10 runs).

Diagnosing Where the Time Goes

Measure before optimizing. Hugo's built-in template metrics show where render time goes with no external tooling:

hugo --templateMetrics --templateMetricsHints

The report ranks templates by cumulative execution time and flags partials that are good caching candidates (via partialCached). On the 10k-page documentation repo used for the numbers in this guide, the top three rows accounted for 71% of render time — two list partials and a single-page hook. For asset-heavy sites, add memory and path diagnostics to surface expensive image processing:

hugo --gc --minify --printMemoryUsage --printPathWarnings

For a stable wall-clock number that you can compare across commits, wrap the full build in hyperfine rather than eyeballing a single time run. hyperfine warms the cache, discards outliers, and reports a mean with standard deviation:

hyperfine --warmup 1 --runs 10 'hugo --gc --minify'

That command produced the 42.1s ± 1.3s cold-equivalent figure in the diagram. The same harness, fully expanded with a containerised environment and an identical synthetic corpus, is documented in How to Benchmark Hugo vs Astro Build Speeds.

Image Processing: Usually the Single Biggest Cost

On content sites with photographs or screenshots, Hugo's image pipeline (Resize, Fit, Fill, and .Process) is frequently the largest line item. Every distinct transform — each width, each target format — is an encode, and on a cold build with no cached resources that work runs for every image on every page.

The fix is twofold. First, emit fewer variants: generate only the widths your srcset actually uses, and pick one modern format rather than three. Second, let Hugo cache the encodes in resources/_gen so a warm build skips them entirely. A typical responsive shortcode that stays lean:

{{/* layouts/shortcodes/img.html */}}
{{ $src := .Page.Resources.GetMatch (.Get "src") }}
{{ $w := slice 480 960 1440 }}
{{ $variants := slice }}
{{ range $w }}
  {{ $variants = $variants | append ($src.Resize (printf "%dx webp q80" .)) }}
{{ end }}
<img
  src="{{ (index $variants 1).RelPermalink }}"
  srcset="{{ range $i, $v := $variants }}{{ if $i }}, {{ end }}{{ $v.RelPermalink }} {{ $v.Width }}w{{ end }}"
  sizes="(max-width: 960px) 100vw, 960px"
  width="{{ (index $variants 2).Width }}" height="{{ (index $variants 2).Height }}"
  loading="lazy" decoding="async" alt="{{ .Get "alt" }}">

Before and after restricting widths from five to three and dropping a redundant AVIF pass on this repo:

Image configEncodes per buildCold build (hyperfine mean)
5 widths × AVIF + WebP~46k42.1s
3 widths × WebP only~17k31.8s

That single change removed roughly 10 seconds before any caching. Reserving dimensions on every <img> also keeps Cumulative Layout Shift down — the asset side of that is covered in Optimizing WebP Images in Hugo Without Plugins.

Render Hooks and Caching

Markdown render hooks let you override how Hugo renders links, images, headings, and code blocks. They are powerful, but a hook fires once per matching element across the whole site — a link hook on a 10k-page repo with 30 links per page runs 300,000 times. Keep hook bodies trivial and avoid per-invocation work like .Site.GetPage lookups or resources.Get calls inside them.

The two highest-leverage moves are wrapping read-only partials in partialCached and keeping hook logic branch-light:

{{/* layouts/_default/_markup/render-link.html */}}
{{- $u := urls.Parse .Destination -}}
{{- $external := $u.IsAbs -}}
<a href="{{ .Destination | safeURL }}"{{ with .Title }} title="{{ . }}"{{ end }}
  {{- if $external }} rel="noopener" target="_blank"{{ end }}>{{ .Text | safeHTML }}</a>

For partials whose output depends only on a stable key, give partialCached a cache key so it computes once:

{{ partialCached "nav.html" . .Section }}

Caching the navigation partial by .Section instead of re-rendering it on every page took render time on this repo from ~14s to ~9s. The full recipe — including the resource cache, render-hook discipline, and warming the cache in CI — lives in Speeding Up Hugo Builds with Render Hooks and Caching.

Template & Taxonomy Optimization

Template cost dominates large builds. The classic blowup is iterating .Site.RegularPages inside a partial that itself runs on every page — that is O(n²) and explodes on large repos. Scope iterations to .Pages, use .Site.GetPage for direct lookups, and wrap expensive read-only partials in partialCached.

If you do not use taxonomies or RSS, disable them so Hugo does not generate those pages. Note that disableKinds is a top-level config key (not under [params]):

# config.toml
disableKinds = ["taxonomy", "term", "RSS"]

[markup.goldmark.renderer]
  unsafe = true

[markup.tableOfContents]
  startLevel = 2
  endLevel = 3

Disabling unused taxonomy and term pages on the benchmark repo removed about 4,000 generated pages and shaved ~3s on its own. Porting heavy logic from another ecosystem — for instance the Jekyll Plugin Ecosystem — rarely translates one-to-one; prefer Hugo's native functions and shortcodes over re-implementing plugin behavior. If template complexity is the real constraint, reconsider the architecture against Astro vs Eleventy for Documentation Sites.

What Actually Speeds Up CI

Since the build is always full, the win in CI is not recomputing image transforms and module downloads every run. Persist Hugo's resource cache and module cache between runs:

name: Build Hugo
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # needed if you use --enableGitInfo for lastmod
      - uses: actions/cache@v4
        with:
          # Hugo's processed-resource and module caches.
          path: |
            resources/_gen
            ~/.cache/hugo_cache
          key: ${{ runner.os }}-hugo-${{ hashFiles('assets/**', 'content/**/*.png', 'content/**/*.jpg') }}
          restore-keys: |
            ${{ runner.os }}-hugo-
      - run: hugo --gc --minify

resources/_gen holds already-processed images and other resources, so a cache hit skips re-encoding them — usually the single biggest CI saving on image-heavy sites. On the benchmark repo, a warm CI build with resources/_gen restored came in at 16.4s versus the cold 42.1s. --enableGitInfo is sometimes recommended here, but be clear about what it does: it attaches git commit metadata (useful for accurate lastmod dates), and does not make the build incremental.

CI scenarioresources/_genBuild (hyperfine mean)
Cold runner, no cacheempty42.1s
Warm runner, cache restoredhit16.4s

Hugo Modules and Data Files

On large repos, two more line items quietly add up. The first is Hugo Modules: if your theme or shared components come in as modules, a cold build with no module cache re-downloads and re-resolves them. Vendoring with hugo mod vendor commits the modules into _vendor/ so the build never hits the network, and persisting ~/.cache/hugo_cache (shown in the CI step above) covers the non-vendored case. The second is data files and getJSON/getCSV remote calls: a resources.GetRemote or getJSON against a slow endpoint blocks the build, and on a large site that fans out across many pages. Fetch remote data once into site.Data rather than per page, and prefer committed local data files for anything that does not change between builds.

# config.toml — keep remote work out of the hot path
[caches]
  [caches.getjson]
    maxAge = "10m"   # cache remote JSON between builds
  [caches.images]
    maxAge = "-1"    # never expire processed images locally

Setting caches.images.maxAge = "-1" tells Hugo to keep processed images indefinitely, which is exactly what you want when you pair it with the resources/_gen cache in CI — the encode happens once and is reused across builds until the source image changes.

Measuring Over Time

Track total build duration and --templateMetrics across commits, and alert in CI when build time jumps more than ~10–15% — a sudden spike almost always traces to a new partial, an unscoped page iteration, or a freshly added batch of unprocessed images. Run the same hyperfine command on a fixed runner so the numbers are comparable; establish a repeatable baseline with How to Benchmark Hugo vs Astro Build Speeds.

Common Pitfalls

  • Unscoped .Site.RegularPages in partials: iterating every page from within a per-page partial is O(n²) and explodes on large repos. Scope to .Pages or use .Site.GetPage, and cache with partialCached.
  • Heavy work inside render hooks: a hook runs once per matching element across the whole site. A .Site.GetPage or resources.Get call inside a link hook multiplies into hundreds of thousands of calls. Keep hook bodies trivial.
  • Over-generating image variants: five widths times two formats is ten encodes per image. Emit only the widths your srcset uses and one modern format unless you have measured a reason for more.
  • Assuming Hugo is incremental: it is not for production builds. Optimize the full build and cache resources/_gen instead of expecting page-level skips.
  • No resource cache in CI: without persisting resources/_gen, every run re-encodes every image. Cache it keyed on your assets and content.

Key Takeaways

  • Profile first with hugo --templateMetrics and a hyperfine wall-clock mean before changing anything.
  • Image processing is usually the biggest cost on content sites; cut variants and cache the encodes in resources/_gen.
  • Render hooks run per element across the whole site — keep them trivial and lean on partialCached.
  • Kill O(n²) page iterations and disable unused taxonomy/RSS kinds.
  • In CI, restore resources/_gen and the module cache; a warm build halved (and more) the cold time on a 10k-page repo.

FAQ

What repository size can Hugo build efficiently?

Hugo comfortably handles 100k+ pages. Build time scales with template complexity and image processing far more than raw page count, so a well-scoped repo with cached resources and disabled taxonomies often builds in under a minute.

Does Hugo support incremental production builds?

No. The hugo command always rebuilds the whole site; only hugo server does fast in-memory partial rebuilds during development. Speed comes from cheaper full builds and CI caching, not from skipping pages.

Where does Hugo spend most of its build time on large repos?

On large content sites the time concentrates in three places: template rendering (especially unscoped page iterations), image processing through the Resize and Fill methods, and Markdown render hooks that run once per matching element. Template metrics show you which one dominates.

How do I catch Hugo build-time regressions in CI?

Log total build duration and template metrics on every commit and alert when build time jumps more than 10 to 15 percent. A sudden spike almost always traces to a new partial, an unscoped page iteration, or a newly added batch of unprocessed images.

Does caching resources/_gen make Hugo incremental?

No. Caching resources/_gen only avoids re-encoding images and other processed resources that have not changed. Hugo still re-renders every page on every build, but skipping image re-encoding is usually the single largest CI saving on media-heavy sites.

Static Site Generators in Production