Images & Responsive Media
Every image uploaded to Shopify — product photos, theme assets — is served from Shopify's CDN, which can resize an image to a huge range of dimensions on the fly just by changing the requested URL. A well-built theme leans on this instead of shipping one large image to every device regardless of screen size.
Requesting a specific size
The image_url filter (the modern replacement for the older img_url) takes a width and/or height and returns a URL for that exact rendition:
<img src="{{ product.featured_image | image_url: width: 600 }}" alt="{{ product.featured_image.alt }}">
Nothing needs to be pre-generated for this to work — Shopify's CDN produces and caches the requested size the first time it's asked for, from the single original file a merchant uploaded.
srcset and responsive images
A fixed size wastes bandwidth on a small phone screen and looks soft on a large retina display. srcset lets the browser choose the best-fitting image from a list of candidates, based on its own screen size and pixel density:
<img
src="{{ product.featured_image | image_url: width: 600 }}"
srcset="
{{ product.featured_image | image_url: width: 300 }} 300w,
{{ product.featured_image | image_url: width: 600 }} 600w,
{{ product.featured_image | image_url: width: 1200 }} 1200w
"
sizes="(min-width: 990px) 50vw, 100vw"
alt="{{ product.featured_image.alt }}"
loading="lazy"
width="600"
height="600"
>
The sizes attribute tells the browser how much of the viewport the image will actually occupy at different breakpoints, which it needs in order to pick the right candidate from srcset before the CSS has even been applied.
The image_tag filter
Writing that full srcset block by hand for every image gets repetitive, so Liquid provides image_tag, which generates a complete, responsive <img> element — including srcset, sizes, width, height, and alt text — from a single filter call:
{{ product.featured_image | image_tag: sizes: '(min-width: 990px) 50vw, 100vw', widths: '300,600,1200' }}
Lazy loading and explicit dimensions
The native loading="lazy" attribute defers loading offscreen images until the user scrolls near them — a meaningful performance win on long product-grid or collection pages. Explicit width and height attributes matter for a different reason: they let the browser reserve the correct space before the image has actually downloaded, preventing the page from visibly jumping around as images load in (a metric called Cumulative Layout Shift).
Video and other media types
Product media isn't limited to images — product.media can also include hosted video and 3D models, each with its own media_type. A template typically checks that type before deciding how to render a given media item:
{% for media in product.media %}
{% case media.media_type %}
{% when 'image' %}
{{ media | image_url: width: 600 }}
{% when 'video' %}
{{ media | media_tag }}
{% endcase %}
{% endfor %}