---
title: Avoid over-fetching product variants
description: >-
  Use `product.options_with_values` instead of iterating `product.variants` to
  load only the option data needed for initial render and avoid expensive
  database queries.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-variant-overfetching
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-variant-overfetching.md
api_name: liquid
---

# 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.

***

## Why

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.

***

## How

### Use `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`:

```liquid
<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.

### When variant access is acceptable

Checking properties without iteration is fine:

```liquid
{%- 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 }}
```

### Defer 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:

```liquid
<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>
```

***

## Examples

### Problematic pattern: iterates all variants

```liquid
{% 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.

### Recommended pattern: uses `options_with_values`

```liquid
<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.

### Acceptable variant usage

```liquid
{%- 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>
```

***

## Testing

* Use the Theme Inspector flame graph to check whether variant iteration is slowing TTFB.
* Compare TTFB in the [Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network) 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.

***

## References

* [`product.options_with_values`](https://shopify.dev/docs/api/liquid/objects/product#product-options_with_values)
* [`product_option_value`](https://shopify.dev/docs/api/liquid/objects/product_option_value) object
* [`product_option_value.available`](https://shopify.dev/docs/api/liquid/objects/product_option_value#product_option_value-available)
* [`product_option_value.selected`](https://shopify.dev/docs/api/liquid/objects/product_option_value#product_option_value-selected)
* [`product_option_value.id`](https://shopify.dev/docs/api/liquid/objects/product_option_value#product_option_value-id)
* [`product.variants`](https://shopify.dev/docs/api/liquid/objects/product#product-variants)
* [`product.variants_count`](https://shopify.dev/docs/api/liquid/objects/product#product-variants_count)
* [`product.selected_or_first_available_variant`](https://shopify.dev/docs/api/liquid/objects/product#product-selected_or_first_available_variant)
* [`for`](https://shopify.dev/docs/api/liquid/tags/for) tag
* [`form`](https://shopify.dev/docs/api/liquid/tags/form) tag
* [`money`](https://shopify.dev/docs/api/liquid/filters/money) filter
* [More To Sell, Less To Manage: Introducing 2,048 Product Variants on Shopify](https://www.shopify.com/blog/2048-variants)
* [Support high-variant products](https://shopify.dev/docs/storefronts/themes/product-merchandising/variants/support-high-variant-products)
* [Defer child product loading in combined listings](https://shopify.dev/docs/storefronts/themes/best-practices/performance/defer-combined-listing-products)
* [Avoid deeply nested Liquid loops](https://shopify.dev/docs/storefronts/themes/best-practices/performance/avoid-deeply-nested-liquid-loops)
* [Use the Section Rendering API for dynamic updates](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-section-rendering-api)

***
