Announcing new Liquid features for better web performance

Announcing new Liquid features for better web performance

We’ve added two exciting new features to the Liquid API to aid in web performance:

  • Default lazy loading for the image_tag for sections further down the page
  • New section properties section.index, section.index0, and section.location for more fine-tuning of image lazy loading as well as async CSS loading.

We created these new section properties specifically to help with three web performance issues that occur due to layout position:

  • Lazy loading images above the fold
  • Asynchronous loading of CSS needed for elements above the fold
  • Not prioritizing the fetch of the the Largest Contentful Paint (LCP) image

If you’d like to better understand the concepts of those issues, read my last article, How layout position impacts three big web performance levers. In this article, we’ll cover:

New default lazy loading for Liquid image_tag Jump to heading

Before, the Liquid image_tag did not apply any logic for any default settings for the img loading attribute. Now, any image_tag past the first three sections of a template will automatically set loading=”lazy” if the loading attribute is not already set (docs). This specific logic may change in the future as we look at real user data and adjust the default value to maximize performance across all Shopify storefronts. The idea is to not lazy load images above the fold which causes a poor user experience and a slower LCP.

To take advantage of this new feature, use the image_tag filter for your images and do not set the loading attribute:

<!-- Input -->
{{ section.settings.image | image_url: width: 300 | image_tag }}

<!-- Output for first three sections (the specific logic may change in future) -->
<img
src="//cdn.shopify.../files/dog.jpg?width=300"
width="300"
height="393" />


<!-- Output for all remaining sections -->
<img
src="//cdn.shopify.../files/dog.jpg?width=300"
loading="lazy"
width="300"
height="393" />

This is a great way to simplify your theme code if your sections are generally large enough that no more than 3 inside of your template or content_for_layout would be “above the fold”. If, however, you need more fine-tuning or if you don’t want to be subject to changes in our default algorithm, we’ve also released some new section properties…

New Liquid section properties Jump to heading

In our previous article, we showed that 82.4% of Shopify pages are lazy loading their LCP image. One reason why this antipattern happens is because before now, theme devs had no contextual information about where a section was located when rendering. Today, that is different. We have three new properties for you (docs):

  • section.index - the 1-based index of the section within its contextual location
  • section.index0 - the 0-based index of the section within its contextual location
  • section.location - the section’s location (can also be thought of as the section’s context or scope)

What is section.location? Jump to heading

Before we cover the index properties, it’s best to understand what we mean by the location. Sections can be rendered in many different locations. Most sections are rendered in the template, but many are also rendered within section groups:

Storefront mockup showing announcement and nav inside a header section group, the template with typical image/text sections, and a footer section group.
A common layout will include a header section group, the template (content for layout), and a footer section group.

When the section is located within the template, its section.location will be template. When the section is located within a section group, its section.location will be the section group type, which can be header, footer, aside, and custom.<name>.

Templates and section groups cover most section locations, but we have one final location type which is static, for a statically rendered section:

The same layout but now there is an independent section between the header section group and the template.
Static sections are outside of templates and section groups.

What are section.index and section.index0? Jump to heading

Both index and index0 are integers representing the index, or order, of the section within its location. For consistency with previous Liquid features, index is the 1-based index while index0 is the 0-based index.

Stated another way, the index restarts within each location, or context. For example, say we have a page with the following layout:

  • Header group
    • Announcement banner section
    • Nav bar section
  • A static section
  • Template
    • Image with text section
    • Another image with text section
    • Featured articles section
  • Footer group
    • Footer section

The location and indices would be the following:

Section location index index0
Announcement banner header 1 0
Nav bar header 2 1
Static section static nil nil
Image with text template 1 0
Another image with text template 2 1
Featured articles template 3 2
Footer footer 1 0

The Online Store Editor is optimized for fast rendering by re-rendering only the section that was updated. This means that if we provided the index, it would not be consistent. Thus, both the index and index0 are forced to be nil when rendering in the Online Store Editor. These new features should be used only for non-visual reasons like optimizing loading speed for real users.

Example use cases and code for using the new section properties Jump to heading

Before we begin, remember that we only need to apply lazy loading for images either below the fold or which are not visible until an interaction (e.g., opening a menu). If an image will always be visible above or even near the fold, it’s safest to eagerly load that image. For example, your main product image should probably be eager loaded.

The use case for selectively applying eager or lazy loading is for sections that can be reused across templates and section groups. The indices will be nil for all other sections.

Lazy load an image based on section.index Jump to heading

Remember that the new default behavior for the image_tag is to set loading to lazy for images after the first three sections, currently. This example manually sets it to lazy after the first two sections to show how you would override that behavior:

{%- liquid
if section.index > 2
assign loading = "lazy"
else
assign loading = "eager"
endif
-%}

{{
section.settings.image
| image_url: width: 1080
| image_tag: loading: loading
}}

Asynchronously load CSS based on section.index Jump to heading

Another problem we see on Shopify storefronts is layout shifts due to late arriving CSS by using the async CSS hack. If you still want to use that hack, but limit it to sections further down the page so that it does not impact Cumulative Layout Shift (CLS), then you can similarly use section.index to selectively apply it:

{% if section.index > 2 %}
<link
rel="stylesheet"
href="{{ 'section-image-banner.css' | asset_url }}"
media="print"
onload="this.media='all'">

<noscript>
{{ 'section-image-banner.css' | asset_url | stylesheet_tag }}
</noscript>
{% else %}
{{ 'section-image-banner.css' | asset_url | stylesheet_tag }}
{% endif %}

Considerations for card lists Jump to heading

For sections that list multiple items with pictures using a forloop, you will need a more complex check for applying lazy loading.

In the following image, the banner image will likely be the LCP element, but we would also want at least the first three items in the featured collection to be eager loaded.

A webpage with a large banner image then a featured collection showing three image cards
For loops of images, use the forloop.index in addtion to the section.index to optimize the lazy loading pattern.

Our forloop will check both section.index and forloop.index to determine whether to set loading to eager or lazy. If the theme supports changing how many columns are shown on desktop, you maybe want to take that number into consideration too. In the above example, If the section.index was 0, I would set the first 6-9 images to eager. If it was 1, I would set the first 3-6 to eager. I might wait until a section.index of 3 to start setting them all to lazy, since partial images might still be visible.

You don’t have to be perfect. You can test simplifying this logic, for example maybe skipping the middle step. I would err on the side of less lazy loading. Then use a tool like WebPageTest to see if there is a significant difference in the performance metrics.

Setting fetchpriority for large image sections likely to be an LCP Jump to heading

Certain sections like image banners have a very high likelihood of being the LCP element because their image is so large. You could reason that if the banner image section is the first or maybe even second section in a template, it will likely be the LCP element. In this case, setting fetchpriority="high" may bump up our LCP speed even faster.

{%- liquid
assign loading = "eager"
assign fetchpriority = "auto"
if section.index == 1
assign fetchpriority = "high"
elsif section.index > 2
assign loading = "lazy"
endif
-%}

{{
section.settings.image
| image_url: width: 1080
| image_tag:
loading: loading,
fetchpriority: fetchpriority
}}

You can simplify your code to this if you take advantage of default lazy loading:

{%- liquid
assign fetchpriority = "auto"
if section.index == 1
assign fetchpriority = "high"
endif
-%}

{{
section.settings.image
| image_url: width: 1080
| image_tag: fetchpriority: fetchpriority
}}

Conclusion Jump to heading

We’re happy to announce these new Liquid features as they should make it easier to develop faster themes. Faster themes lead to faster stores and higher conversions. We’d love to hear your feedback and success stories.

Cover photo by okeykat on Unsplash

CONTENTS

Read similar articles tagged...

Back to blog