I’m working on optimizing an eCommerce site built with Hugo. I’m caching some of our most used partials based on the feedback from running hugo --templateMetrics --templateMetricsHints
but the results I’m getting are not always as expected.
Example: We have partial that imports a JSON file, does some logic on it, and returns a Hugo dictionary. We use that dictionary in a number of different places in our site, so the partial is always cached (I have triple-checked that it is always called with partialCached
and it is never given any caching args).
However, despite this, I see via templateMetrics that the partial is run 11 times. This isn’t even something that runs on every page, (although it is used a lot):
cache cumulative average maximum
potential duration duration duration count template
----- ---------- -------- -------- ----- --------
0 4.8749971s 443.181554ms 612.9998ms 11 partials/products/get-all-products.html
In contrast, our footer partial, which runs on almost every single page of the site, is only run 3 times after caching it. This is closer to what I’d expect (since we have 2 different languages) although I’m still not sure what’s going on with it running the third time.
cache cumulative average maximum
potential duration duration duration count template
----- ---------- -------- -------- ----- --------
100 4.0015ms 1.333833ms 3.0009ms 3 partials/footer.html
Note that the footer partial was running 353 times before we started caching it.
The only differences that I can think of between these two partials are that the first one involves a JSON get, a YML get, and has its value passed to js.Build as a “param” on a few different occasions, but I can’t think of why any of those things should matter unless this is some sort of race condition with how the build runs under the hood.
TL;DR:
So my question is: what is it that I don’t understand about partial caching? What other things cause a cached partial to be re-run besides language and caching arguments? Or is this a bug?
For further details, the basic outline of the file in question is like this:
{{/* Import some JSON data and then import data from a YML file */}}
{{- $products := getJSON $.Site.Params.shopifyProducts -}}
{{- $coreProducts := $.Site.Data.en.coreproducts.products -}}
{{/* Do a bunch work with all that raw data and spit out a single Hugo dictionary */}}
{{/* ... */}}
{{/* Return dictionary */}}
{{ return $allProductMetadata }}
If you want to see the entire file in question, see below:
<!-- prettier-ignore -->
{{/****************
* This partial returns a Hugo dictionary containing product
metadata for all Products.
* The variants property of each item has been preserved for compatibility, but
at this time, it should not ever be needed as all relevant data from .variants[0]
is already added to the top level properties of each product.
* If you do not use the "with" pattern to
call this, that data will simply be dumped onto the DOM. Do not do that.
* If you only need info about one or two products, you probably want to use one of the following partials instead:
These two partials both wrap 'get-all-products' with a 'where range' statement in order to pull out only the product object you want
- '/partials/products/get-product-by-key'
- '/partials/products/get-product-by-id'
****************/}}
{{- $products := getJSON $.Site.Params.shopifyProducts -}}
{{- $coreProducts := $.Site.Data.en.coreproducts.products -}}
{{/* Dev-store config */}}
{{- $isDevStore := false -}}
{{- if eq hugo.Environment "devstore" -}}
{{- $isDevStore = true -}}
{{- end -}}
{{ warnf (print "Generating all-products array with IsDevstore = " $isDevStore ) }}
{{ $allProductMetadata := slice }}
{{- range $idx, $shopifyProduct := $products.products -}}
{{ $coreProduct:= "" }}
{{/* Find corresponding coreproducts.yml entry if it exists */}}
{{- range $idy, $cProduct := $coreProducts -}}
{{ if (eq (string $shopifyProduct.id) (cond $isDevStore (string $cProduct.dev_id) (string $cProduct.id) ) ) }}
{{ $coreProduct = $cProduct }}
{{ if (not (reflect.IsMap $coreProduct) ) }}
{{ errorf (print "CoreProduct item was somehow not a map:") }}
{{ errorf (print $coreProduct ) }}
{{ end }}
{{ end }}
{{ end }}
{{ $modifiedProduct := $shopifyProduct }}
{{/* Rename or delete properties as needed from existing properties. */}}
{{/**********************************************************************************************************************
Note: 'variant' can only be nil if there are multiple items in 'variants' or
add to cart actions will fail If we want to be able to choose from multiple
variants though, 'variant' MUST be nil or add to cart will always just add the
first variant See 'window.selectVariant()' in 'product.js' and
'partials/products/variant-select.html' for details.
**********************************************************************************************************************/}}
{{ $modifiedProduct = merge $modifiedProduct (dict "admin_graphql_api_id" nil ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "barcode" (index ($modifiedProduct.variants) 0).barcode ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "category" $modifiedProduct.product_type ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "gtin12" (index ($modifiedProduct.variants) 0).barcode ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "body_html" nil ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "compare_at_price" (index ($modifiedProduct.variants) 0).compare_at_price ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "grams" (index ($modifiedProduct.variants) 0).grams ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "images" nil ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "inventory_management" (index ($modifiedProduct.variants) 0).inventory_management ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "inventory_policy" (index ($modifiedProduct.variants) 0).inventory_policy ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "inventory_quantity" (index ($modifiedProduct.variants) 0).inventory_quantity ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "key" $modifiedProduct.handle ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "link" (print "/products/" $modifiedProduct.handle "/" ) ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "name" $modifiedProduct.title ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "price" (index ($modifiedProduct.variants) 0).price ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "sku" (index ($modifiedProduct.variants) 0).sku ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "variant" (cond (lt (len $modifiedProduct.variants) 2) (index ($modifiedProduct.variants) 0).id nil ) ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "variantTitle" (index ($modifiedProduct.variants) 0).title ) }}
{{/* Substitute and/or add in values from core products (if available) */}}
{{ with $coreProduct }}
{{ $modifiedProduct = merge $modifiedProduct (dict "active" .suggestion.active ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "affirm" .affirm ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "hide_guarantee" .hide_guarantee) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "href" .pdp) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "id" (cond $isDevStore .dev_id .id)) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "isCoreProduct" true ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "image" .cart_thumbnail ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "key" .key ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "link" .pdp) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "long_title" .long_title ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "name" .name ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "oos_form" .oos_form ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "relevant" .suggestion.relevant ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "servings" .servings ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "thumbnail" .cart_thumbnail ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "variant" (cond (lt (len $modifiedProduct.variants) 2) (cond $isDevStore .dev_variant .variant) nil ) ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "defaultVariantName" .manual_variant_name ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "weight" .suggestion.weight ) }}
{{ with .special_shipping_exclusions }}
{{ $modifiedProduct = merge $modifiedProduct (dict "specialShippingExclusions" . ) }}
{{ end }}
{{ with .subscription }}
{{ $subscriptionData := "" }}
{{ if (reflect.IsSlice .) }}
{{ $subscriptionData = slice }}
{{ range $idz, $subData := . }}
{{ $subscriptionDataItem := dict }}
{{ $subscriptionDataItem = dict "groupid" (cond $isDevStore .dev_group_id .group_id) }}
{{ $subscriptionDataItem = merge $subscriptionDataItem (dict "discount" .discount ) }}
{{ $subscriptionDataItem = merge $subscriptionDataItem (dict "options" (dict "1" .option_1 "2" .option_2 )) }}
{{ $subscriptionDataItem = merge $subscriptionDataItem (dict "text" .text ) }}
{{ $subscriptionDataItem = merge $subscriptionDataItem (dict "bundle_text" .bundle_text ) }}
{{ $subscriptionDataItem = merge $subscriptionDataItem (dict "disabled" .disabled ) }}
{{ $subscriptionData = $subscriptionData | append (merge (index $modifiedProduct.variants $idz) (dict "subscription" $subscriptionDataItem)) }}
{{ end }}
{{ else if (reflect.IsMap .) }}
{{ $subscriptionData = dict "groupid" (cond $isDevStore .dev_group_id .group_id) }}
{{ $subscriptionData = merge $subscriptionData (dict "discount" .discount ) }}
{{ $subscriptionData = merge $subscriptionData (dict "options" (dict "1" .option_1 "2" .option_2 )) }}
{{ $subscriptionData = merge $subscriptionData (dict "text" .text ) }}
{{ $subscriptionData = merge $subscriptionData (dict "bundle_text" .bundle_text ) }}
{{ $subscriptionData = merge $subscriptionData (dict "disabled" .disabled ) }}
{{ else }}
{{ errorf (print "Core product subscription data was neither map nor array for: " $coreProduct.name ) }}
{{ end }}
{{/* If there is more than one subscription data object, assign each object to the corresponding variant and set top level subscription to 'seeVariants' */}}
{{ if (reflect.IsSlice $subscriptionData ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "variants" $subscriptionData ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "subscription" "seeVariants" ) }}
{{ else }}
{{ $modifiedProduct = merge $modifiedProduct (dict "subscription" $subscriptionData ) }}
{{ end }}
{{ end }}
{{ with .bundles }}
{{ $bundleData := dict "id" (cond $isDevStore .dev_bundled_id .bundled_id) }}
{{ $bundleData = merge $bundleData ( dict "variant" (cond $isDevStore .dev_bundled_variant .bundled_variant) ) }}
{{ $bundleData = merge $bundleData ( dict "bundleTitle" .bundled_title ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "bundle" $bundleData ) }}
{{ end }}
{{ end }}
<!-- prettier-ignore -->
{{/***********************************
Set 'inStock' property:
// Where the returned value is what will be assigned to inStock
- Logic summary (simplified and in JS style):
if (isNotCoreProduct) return true; // Only core products can ever be out of stock
else if (coreproduct.currently_out_of_stock === true) return false; // Always overrides everything else
else if (coreproduct.never_out_of_stock === true) return true; // Overrides shopify
else if (shopify.inventory_quantity < 10 && shopify.inventory_policy === "deny" ) return false; // If there is no stock (at build time) then it is out of stock unless explicitly set to 'allow'
else return true; // If no conditionals were triggered, then it is available
- Note: To work with the Hugo logic, the actual code below is almost exactly backwards of the way it is written above
***********************************/}}
{{ $modifiedVariantArray := slice }}
{{ $totalVariantsInStock := 0 }}
{{ $totalVariantsOutOfStock := 0 }}
{{ range $index, $variant := $modifiedProduct.variants }}
{{ $inStock := true }}
{{ with $coreProduct }}
{{ with $variant.inventory_policy }}
{{ with $variant.inventory_quantity }}
{{ if and ( le (int $variant.inventory_quantity) 10 ) (eq $variant.inventory_policy "deny") }}
{{ $inStock = false }}
{{ end }}
{{ end }}
{{ end }}
{{ with $coreProduct.currently_out_of_stock }}
{{ if (or (eq $coreProduct.currently_out_of_stock true ) (eq $coreProduct.currently_out_of_stock "true" )) }}
{{ $inStock = false }}
{{ end }}
{{ else }}
{{ with $coreProduct.never_out_of_stock }}
{{ if (or (eq . true) (eq . "true")) }}
{{ $inStock = true }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* Track total true and false values across variants so we can easily see if they are all the same (for this product) at the end */}}
{{ if (eq $inStock true) }}
{{ $totalVariantsInStock = add $totalVariantsInStock 1 }}
{{ else }}
{{ $totalVariantsOutOfStock = add $totalVariantsOutOfStock 1 }}
{{ end }}
{{ $modifiedVariant := merge $variant (dict "inStock" $inStock ) }}
{{ $modifiedVariantArray = $modifiedVariantArray | append $modifiedVariant }}
{{ end }}
{{ $modifiedProduct = merge $modifiedProduct (dict "variants" $modifiedVariantArray ) }}
{{/* If all variants have the same value for inStock, then set inStock for the product to that value */}}
{{ if (or (eq $totalVariantsInStock 0) (eq $totalVariantsOutOfStock 0) ) }}
{{ $modifiedProduct = merge $modifiedProduct (dict "inStock" (index $modifiedVariantArray 0).inStock ) }}
{{ end }}
{{ $allProductMetadata = $allProductMetadata | append $modifiedProduct }}
{{ end }}
{{ return $allProductMetadata }}
This is from a private repo, so I’m unable to share our entire website’s code with you, but I’m happy to provide any additional details.
$ hugo version
Hugo Static Site Generator v0.80.0/extended windows/amd64 BuildDate: unknown
PS: I also just noticed that the footer partial still shows a “cache potential” of 100, while the other one has a “cache potential” of 0. What do those numbers actually mean, and how does a fully cached file still have a “cache potential” of 100?