Right, so you’ve met {{ partial }} and you’re probably thinking, “These are great! I can break my site into logical, reusable chunks.” And you’d be right. But you’re about to hit a wall. A big, slow, frustrating wall. Every time you change a single line of CSS, Hugo has to rebuild your entire site’s structure because it can’t be sure that change didn’t affect the header partial used on every single page. It’s maddening.

This is where partialCached saunters in, puts on a pair of sunglasses, and says, “I got this.” It is, without a doubt, one of the most powerful tools in Hugo’s arsenal for making your build times scream. The concept is simple: instead of re-rendering a partial from scratch on every single page that calls it, Hugo renders it once and then caches the result, reusing that cached HTML for subsequent calls. The performance gains aren’t just incremental; they’re downright dramatic, especially for large sites with deeply nested partials.

But—and this is a huge but—this is pure, uncut performance heroin. It’s incredibly powerful, but you have to respect it or you’ll shoot your foot off. The entire challenge with partialCached is answering one question: When should the cache be invalidated and the partial re-rendered?

The Basic Syntax: It Couldn’t Be Easier

You use it almost exactly like a regular partial. The simplest form just takes the partial’s name.

{{/* layouts/_default/baseof.html */}}
<head>
  ...
  {{ partialCached "head/seo.html" . }}
  ...
</head>

In this case, Hugo will render seo.html once and then serve that same cached HTML blob for every single page on your site. This is fantastic for something like a <head> section that is truly, completely identical across every page. But for anything that needs to vary at all, this is a disaster. Your page-specific title and description? They’d be stuck on whatever was on the first page Hugo built.

The Secret Sauce: Cache Variants

This is where partialCached gets clever. You can provide a list of “cache variants” as additional arguments. Hugo uses the combination of the partial name and these values to create a unique key for the cache. The partial is only re-rendered when this unique combination is encountered for the first time.

{{ partialCached "head/seo.html" . .Permalink .Title }}

Now, the cache key is based on the page’s permanent link and its title. Two pages with different URLs will get different cached versions. This is what you want for 99% of your dynamic partials. You pass in the bits of data that, if they change, should cause the partial to re-render.

Think of the cache variants like the ingredients list for a recipe. The same list of ingredients (cache key) will always produce the same cake (cached HTML). A different ingredient (a different .Permalink) means you need to bake a new cake.

A Real-World, Runnable Example

Let’s say you have a “related articles” partial. It’s computationally expensive because it uses .Site.RegularPages.Related. You absolutely do not want to calculate this on every build for every page.

{{/* layouts/partials/related-articles.html */}}
{{ $related := .Site.RegularPages.Related . | first 3 }}
<h2>You Might Also Like</h2>
<ul>
{{ range $related }}
  <li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>

You’d cache it using the current page’s context (.) as the variant, because the related articles are unique to each page:

{{/* layouts/posts/single.html */}}
<article>
  ...
  {{ partialCached "related-articles.html" . . }}
</article>

Wait, why . .? The first . is the context passed into the partial. The second . is the cache variant. They are separate arguments. In this case, we’re using the same object for both, which is common.

Common Pitfalls and How to Avoid Them

  1. Under-Caching: This is the original problem: not using partialCached for expensive partials and watching your build times balloon. If a partial doesn’t need to be unique per page, or can be cached with a simple variant, you’re leaving massive performance gains on the table.

  2. Over-Caching (The Big One): This is the trap. You cache a partial but forget to include a crucial variant. The most classic example? Caching a partial that uses a .Param from your site config.

    {{/* 🚨 DANGER: This is BAD */}}
    {{ partialCached "footer.html" . }}
    
    {{/* layouts/partials/footer.html */}}
    <footer>
      © {{ now.Year }} {{ .Site.Params.AuthorName }} <!-- Oh no! -->
    </footer>
    

    The first page built in the year 2023 will cache the footer with “© 2023 My Name”. On January 1st, 2024, every page on your site will still proudly display “© 2023 My Name” because Hugo keeps serving the old cached version. The fix is to use now.Year as a cache variant:

    {{/* ✅ GOOD: Cache by year */}}
    {{ partialCached "footer.html" . now.Year }}
    
  3. Caching Context-Incorrect Partials: If your partial expects a specific context (e.g., it uses .Title), you must ensure the context you pass to partialCached (the first argument after the name) is correct. The caching doesn’t change the partial’s logic, it just changes how often that logic is executed.

Best Practices from the Trenches

  • Profile First: Don’t just guess. Use hugo --templateMetrics --templateMetricsHints to see which of your partials are taking the most time to render. Attack the slowest ones first with partialCached.
  • Start Simple: If a partial is truly static (like a site-wide navbar with no dynamic content), cache it with no variants: {{ partialCached "navbar.html" . }}.
  • Cache Aggressively, but Variant Carefully: For any dynamic partial, your first thought should be, “What is the minimal set of values that, when changed, require a re-render?” For a page-specific partial, it’s often just .. For a partial that uses a site parameter, it might be .Site.Params.MySetting.
  • When in Doubt, Bust the Cache: The nuclear option for development is to run hugo --ignoreCache. This ignores all caches (including partialCached) and rebuilds everything from scratch. It’s slow, but it’s the best way to check if a weird bug is due to your caching strategy.

partialCached is Hugo acknowledging that most of the web is repetitive and we shouldn’t apologize for optimizing for that. Use it wisely, and you’ll go from watching the build counter tick up for minutes to watching it blast through thousands of pages in seconds. It’s that good.