Skip to main content

Load above-the-fold and below-the-fold sections differently with section.index

Use section.index, section.index0, and section.location Liquid properties to conditionally optimize loading strategies and CSS application based on section position.


Before these properties, theme developers couldn't determine section position, leading to common anti-patterns: lazy loading all images, including the LCP image; async loading all CSS, including above-the-fold CSS; and no fetch priority hints, because the first section couldn't be identified. These properties enable position-aware optimization that prevents lazy loading the LCP image while still lazy loading images outside the initial viewport, and loads CSS for content visible in the initial viewport synchronously while loading CSS for content outside the initial viewport asynchronously.


Anchor to The three position propertiesThe three position properties

  1. section.index: 1-based position (1, 2, 3, 4...).
  2. section.index0: 0-based position (0, 1, 2, 3...).
  3. section.location: template, static, content_for_index, preset, a section group type such as header, footer, or aside, or custom.<value> for a custom section group.

Anchor to [object Object], is nil in three contextssection.index is nil in three contexts

section.index and section.index0 are nil when the section isn't rendered from a positioned list:

  • The section is rendered as a static section with {% section 'name' %}, such as from theme.liquid.
  • The section is rendered in the online store editor.
  • The section is rendered through the Section Rendering API.

This matters because comparisons against nil are falsey in Liquid. nil <= 2 doesn't raise an error and doesn't evaluate to true. It evaluates to false, so a condition written as {% if section.index <= 2 %} takes its else branch. If the else branch lazy-loads the image, then your LCP image is lazy-loaded in the online store editor. If there's no else branch, then the image isn't rendered at all.

Write the condition so that nil falls to the safe side. Test the below-the-fold case as a positive section.index > N comparison, and let every other case, including nil, fall through to the eager or synchronous branch:

{% unless section.index > 2 %}
{%- comment -%} Sections 1 and 2, and any context where section.index is nil {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'eager' }}
{% else %}
{%- comment -%} Sections 3 and later {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'lazy' }}
{% endunless %}

The image_tag filter's own default already works this way. When you don't pass a loading argument, image_tag loads the image eagerly for the first three sections and whenever section.index0 is nil. Hand-written gating that compares section.index with <= produces the opposite behavior in exactly the contexts where the built-in default is careful.

Anchor to [object Object], counts per location, not per pagesection.index counts per location, not per page

The counter restarts at 1 for each rendered location. A section group, a JSON template, and content_for_index each maintain their own numbering, so the first section of the footer group and the first section of the template are both index 1. Disabled sections don't consume an index.

Because of this, section.index == 1 means "first in its location," not "first on the page." Combine it with section.location when you need page-level position.

Anchor to Use case 1: Conditional lazy loadingUse case 1: Conditional lazy loading

Prevent lazy loading the LCP image by eagerly loading images in the first few sections:

{% unless section.index > 3 %}
{%- comment -%} First three sections, and nil contexts: eager load {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'eager' }}
{% else %}
{%- comment -%} Sections 4 and later: lazy load {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'lazy' }}
{% endunless %}

Anchor to Use case 2: Conditional fetch priorityUse case 2: Conditional fetch priority

{% if section.index == 1 %}
{%- comment -%} First section only: high priority {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'eager', fetchpriority: 'high' }}
{% elsif section.index > 3 %}
{%- comment -%} Sections 4 and later: lazy load {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'lazy' }}
{% else %}
{%- comment -%} Sections 2 and 3, and nil contexts: eager, normal priority {%- endcomment -%}
{{ image | image_url: width: 1000 | image_tag: loading: 'eager' }}
{% endif %}

Anchor to Use case 3: Conditional async CSSUse case 3: Conditional async CSS

{% unless section.index > 3 %}
{%- comment -%} Above the fold, and nil contexts: render-blocking CSS {%- endcomment -%}
<link rel="stylesheet" href="{{ 'section.css' | asset_url }}">
{% else %}
{%- comment -%} Below the fold: async CSS {%- endcomment -%}
<link rel="stylesheet" href="{{ 'section.css' | asset_url }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="{{ 'section.css' | asset_url }}"></noscript>
{% endunless %}

Anchor to Use ,[object Object]Use section.location

For header-specific or footer-specific logic:

{% if section.location == 'header' %}
{%- comment -%} The header is always above the fold {%- endcomment -%}
{{ logo | image_url: width: 200 | image_tag: loading: 'eager' }}
{% endif %}

Comprehensive image loading strategy:

{% if section.index == 1 %}
{%- comment -%} Hero section: maximum priority {%- endcomment -%}
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400, 600, 800, 1000, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero-image'
}}
{% elsif section.index > 3 %}
{%- comment -%} Sections 4 and later: lazy load {%- endcomment -%}
{{ section.settings.image
| image_url: width: 1000
| image_tag:
loading: 'lazy',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
{% else %}
{%- comment -%} Sections 2 and 3, and nil contexts: eager load, normal priority {%- endcomment -%}
{{ section.settings.image
| image_url: width: 1000
| image_tag:
loading: 'eager',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
{% endif %}

  • Test position-dependent logic on the storefront, not in the theme editor. section.index is nil in the editor, so reordering sections there never changes the value and can't verify the logic.
  • Reorder the sections in the editor, save, and then load the storefront preview to check that the loading logic adjusts correctly.
  • Open the theme editor and confirm that every image still renders. A missing image there means a condition compares section.index without handling nil.
  • Test across page types, such as home, product, and collection pages, because the index restarts at 1 in each section group and template.
  • Check the Priority column in the Network panel to verify that the LCP image has fetchpriority="high" and non-critical resources aren't prioritized too early.
  • Use the Chrome DevTools Network panel to verify loading strategies and resource priorities.


Was this page helpful?