Skip to content

Performance

Storefront speed decides whether a customer stays. Most of what costs a YouCan theme its performance is images and JavaScript, in that order.

Images

Use WebP or AVIF

Ship modern formats for the images that live in the theme's assets directory. WebP is typically 25 to 35% smaller than an equivalent JPEG, and AVIF smaller still, at the same visual quality. PNG is the worst offender: use it only for images that genuinely need lossless colour or transparency that WebP can't give you, which in practice is almost never.

liquid
<img src="{{ 'hero.webp' | asset_url }}" width="1440" height="720" alt="">

When you need a fallback for older browsers, let the browser choose with <picture>. It takes the first <source> whose type it supports:

liquid
<picture>
  <source srcset="{{ 'hero.avif' | asset_url }}" type="image/avif">
  <source srcset="{{ 'hero.webp' | asset_url }}" type="image/webp">
  <img src="{{ 'hero.jpg' | asset_url }}" width="1440" height="720" alt="">
</picture>

Seller-uploaded images come back in whatever format they uploaded, so this applies to the artwork you ship with the theme. For seller images, the lever you have is picking the right size.

Request the size you'll display

Store images expose several renditions. Use the smallest one that still looks right at the size you render it, rather than reaching for original everywhere:

PropertyUse for
smallThumbnails and cart line items.
mediumProduct cards in a grid.
largeFeatured images and single-column layouts.
originalFull-bleed heroes and zoom views.
liquid
<img src="{{ product.preview_image.medium }}" alt="{{ product.title }}">

Combine renditions with srcset so the browser picks per viewport, and use <source media> when the mobile and desktop images should differ:

liquid
<picture>
  <source srcset="{{ settings.hero_mobile.src }}" media="(max-width: 768px)">
  <source srcset="{{ settings.hero_desktop.src }}" media="(min-width: 769px)">
  <img src="{{ settings.hero_desktop.src }}" alt="" width="1440" height="720">
</picture>

Always set dimensions

Every <img> needs width and height, or CSS that reserves the space. Without them the page reflows when the image lands, which is the most common source of layout shift. The image object carries width, height, and aspect_ratio:

liquid
<img
  src="{{ section.settings.image.src }}"
  width="{{ section.settings.image.width }}"
  height="{{ section.settings.image.height }}"
  alt="{{ section.settings.image.alt }}"
>

Load the right images eagerly

The largest image in the first viewport is usually what the browser measures for Largest Contentful Paint. Load it eagerly and mark it as important. Lazy-load everything below the fold:

liquid
{% for block in section.blocks %}
  <img
    src="{{ block.settings.image.src }}"
    {% if forloop.first %}
      loading="eager" fetchpriority="high"
    {% else %}
      loading="lazy"
    {% endif %}
  >
{% endfor %}

Two things to avoid: never lazy-load the hero image, and don't hide it behind a fade-in animation. The paint doesn't count until it's visible.

Prefer <img> over a CSS background-image for meaningful images. The browser's preload scanner finds <img> while the HTML is still parsing; a background image isn't discovered until the stylesheet has been fetched and applied.

JavaScript

Defer everything you can. script_tag_deferred should be the default, and a blocking script_tag should be the exception you can justify:

liquid
{{ '_global.js' | asset_url | script_tag_deferred }}

Beyond that:

  • Render content server-side. A product grid built in JavaScript delays the paint and is invisible to search engines. Build it in Liquid.
  • Load on interaction. Cart drawers, filter panels, and modals don't need their code at page load. Pull it in with a dynamic import() from the event handler that opens them.
  • Build hidden DOM lazily. A drawer that isn't open doesn't need to be in the document.
  • Debounce and throttle. Handlers on scroll, resize, and input run constantly, and slow handlers show up directly as poor interaction responsiveness.
  • Audit what you ship. Scripts accumulate. Anything you can't point at a feature for should come out.

Styles

Consolidate stylesheets. Each <link> is a request, and each one forces the browser to recalculate style.

Load the base stylesheet in the layout, and keep section stylesheets in their sections so a page only pays for what it renders. See JavaScript and styles.

Animate with transform and opacity. Animating top, left, width, or height forces layout on every frame.

Fonts

Every web font is a blocking resource between the customer and readable text.

  • Ship the fewest weights you can. Two is usually enough.
  • Self-host in assets rather than pulling from a third-party host, which costs a DNS lookup and a fresh connection.
  • If you must use an external font host, preconnect to it so the connection is warm by the time the CSS asks for the file.
  • Consider a system font stack. It costs nothing and renders immediately.
liquid
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Liquid

Liquid runs before anything reaches the browser, so an expensive template delays the whole page.

  • Don't nest loops over large collections. Looping every variant inside every product scales badly on a large catalogue.
  • Compute once, outside the loop. Assign the result of a filter chain to a variable rather than recomputing it per iteration.
  • Filter before you loop, not with an {% if %} inside it.
  • Limit what you render. Use {% paginate %} and section settings that cap how many items are shown, instead of rendering an entire collection.
liquid
{%- assign currency = store.currency.symbol -%}
{% for product in collection.products %}
  <span>{{ product.price | money }}{{ currency }}</span>
{% endfor %}

Resource hints

Use preload for the one or two resources the browser discovers late and needs early, usually a hero image or a font file. Preloading everything is the same as preloading nothing, because it competes with the browser's own prioritization.

liquid
{{ 'hero.webp' | asset_url | preload_tag }}

Measure on mobile

Test with CPU throttling and a slow network profile, not on a desktop over office wifi. Check that content fits the viewport without horizontal scroll, and that tap targets are large enough to hit reliably.