Skip to main content

Avoid over-fetching product variants

Load only the variant data you need for initial page render instead of iterating over all product variants, which forces expensive database queries that slow TTFB.


Iterating over product.variants forces the server to load and process all variant data during page rendering. For products with hundreds or thousands of variants (Shopify supports up to 2,048 variants), this significantly increases TTFB by loading unnecessary data.

Most product pages need only option names and values for the option picker, not full variant data for every combination. Loading all variants is wasteful when you need only the selected variant's details.


Anchor to Use ,[object Object], for option pickersUse options_with_values for option pickers

The product.options_with_values object provides option data without loading full variant objects. It significantly reduces rendering time while still providing availability information through product_option_value.available and selection state through product_option_value.selected.

Replace variant iteration with product.options_with_values:

<div class="option-value-selectors" data-section-id="{{ section.id }}">
{%- for option in product.options_with_values -%}
<fieldset>
<legend>{{ option.name }}</legend>

{%- for option_value in option.values -%}
<input
id="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}"
type="radio"
name="{{ section.id }}-{{ option.name }}-{{ option.position }}"
value="{{ option_value | escape }}"
{% if option_value.selected %}checked{% endif %}
{% unless option_value.available %}disabled{% endunless %}
data-option-value-id="{{ option_value.id }}"
/>
<label for="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}">
{{ option_value }}
</label>
{%- endfor -%}
</fieldset>
{%- endfor -%}
</div>

Benefits:

  • Loads only option data, not all variant data.
  • Faster rendering with reduced TTFB.
  • Works with products of any variant count.
  • product_option_value.available provides availability without loading variants.
  • product_option_value.selected simplifies state management.

Anchor to When variant access is acceptableWhen variant access is acceptable

Checking properties without iteration is fine:

{%- comment -%} OK: not iterating, just checking {%- endcomment -%}
{% if product.variants.size > 1 %}
<p>This product has options</p>
{% endif %}

{%- comment -%} OK: accessing the first variant {%- endcomment -%}
{{ product.variants.first.price }}

{%- comment -%} OK: using variants_count {%- endcomment -%}
{{ product.variants_count }}

Anchor to Defer variant loading with the Section Rendering APIDefer variant loading with the Section Rendering API

Use JavaScript to fetch updated variant information when a buyer selects an option value. Read the selected option value IDs from the picker's data-option-value-id attributes and pass them as the option_values parameter. You can pass the IDs in any order, and a partial set is fine: options that you leave out fall back to their first available value.

Scope the lookup to the picker that fired the event with event.target.closest(). A document-wide querySelector returns the first match on the page, which is the wrong section when a page renders more than one product:

<div class="product-info" id="ProductInfo-{{ section.id }}">
<div class="price">{{ product.price | money }}</div>
<div class="inventory">{{ product.selected_or_first_available_variant.inventory_quantity }} in stock</div>
</div>

<script>
function updateVariantInfo(event) {
const selectors = event.target.closest('.option-value-selectors');
const sectionId = selectors.dataset.sectionId;

const optionValues = Array.from(
selectors.querySelectorAll('input[type="radio"]:checked')
)
.map(({dataset}) => dataset.optionValueId)
.join(',');

fetch(`${window.location.pathname}?section_id=${sectionId}&option_values=${optionValues}`)
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, 'text/html');
const productInfo = document.getElementById(`ProductInfo-${sectionId}`);

productInfo.innerHTML = html.querySelector(`#ProductInfo-${sectionId}`).innerHTML;
selectors.innerHTML = html.querySelector('.option-value-selectors').innerHTML;
});
}
</script>

Anchor to Problematic pattern: iterates all variantsProblematic pattern: iterates all variants

{% form 'product', product %}
<select name="id">
{%- for variant in product.variants -%}
<option value="{{ variant.id }}">
{{ variant.title }} - {{ variant.price | money }}
</option>
{%- endfor -%}
</select>
{% endform %}

This loads and iterates through every variant, dramatically slowing TTFB for high-variant products.

<div class="option-value-selectors" data-section-id="{{ section.id }}">
{%- for option in product.options_with_values -%}
<fieldset>
<legend>{{ option.name }}</legend>
{%- for option_value in option.values -%}
<input
id="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}"
type="radio"
name="{{ section.id }}-{{ option.name }}-{{ option.position }}"
value="{{ option_value | escape }}"
{% if option_value.selected %}checked{% endif %}
{% unless option_value.available %}disabled{% endunless %}
data-option-value-id="{{ option_value.id }}"
onchange="updateVariantInfo(event)"
/>
<label for="{{ section.id }}-{{ option.position }}-{{ forloop.index0 }}">
{{ option_value }}
</label>
{%- endfor -%}
</fieldset>
{%- endfor -%}
</div>

Loads only option data, and works efficiently regardless of variant count.

Anchor to Acceptable variant usageAcceptable variant usage

{%- comment -%} Check count without iteration {%- endcomment -%}
{% if product.variants.size > 1 %}
<div class="variant-picker">...</div>
{% endif %}

{%- comment -%} Access the first or selected variant {%- endcomment -%}
{{ product.selected_or_first_available_variant.price | money }}
{{ product.variants.first.image | image_url: width: 600 | image_tag }}

{%- comment -%} Use the variant count {%- endcomment -%}
<p>{{ product.variants_count }} options available</p>

  • Use the Theme Inspector flame graph to check whether variant iteration is slowing TTFB.
  • Compare TTFB in the Chrome DevTools Network panel before and after switching to product.options_with_values.
  • Test with high-variant products. Create a test product with 100 or more variants to see the impact.
  • Check the rendering time difference between products with 10 variants and products with 1,000 variants.


Was this page helpful?