Image and Video Optimization for Shopify: The Complete Guide
Images are the number one drag on Shopify store speed. They account for roughly 27% of total page weight, they dominate your Largest Contentful Paint score, and they are where the majority of "why is my store so slow" problems trace back to.
The good news is that image and video optimization is also the highest-ratio work you can do. Get it right and you can cut page weight by 60 to 70% while making the store look exactly the same to the shopper. 😎
This guide covers the full picture. How Shopify actually serves media, the correct formats and sizing, lazy loading, layout stability, video handling, and the specific Liquid filters and tags you should be using in your theme to get all of this for free.
Why images matter more than almost anything else
When a shopper loads a product page, the largest visible element is almost always an image, usually the hero or the main product photo. That element is what LCP measures, and LCP under 2.5 seconds is the single highest-impact Core Web Vital on Shopify. So your images are not just "assets," they are the thing standing between the shopper and a page that feels fast.
Unoptimized images cause three separate problems at once:
- They slow LCP. Big files take longer to download and paint.
- They cause layout shift. Images without set dimensions make the page jump, hurting CLS.
- They waste bandwidth. Especially painful on the mobile connections that make up most of your traffic.
Fix images well and you improve all three metrics simultaneously.
First, understand what Shopify does for you automatically
Before changing anything, it helps to know what the platform already handles, because it changes what you should and should not do manually.
Shopify serves all theme and product media through its CDN, and that CDN automatically detects which image formats the visitor's browser supports (WebP, AVIF, and so on) and picks the format that gives the best quality at the smallest size for that specific visitor. This is documented behavior of the image filters, when you serve an image through Shopify's URL filters, format negotiation happens at the edge without you specifying anything.
This has a practical and slightly counterintuitive consequence: you generally should not try to force WebP or AVIF yourself in Liquid. The format parameter on Shopify's image filters only supports jpg and pjpg (progressive JPEG) conversions. It does not let you force WebP or AVIF, because Shopify is already choosing the optimal modern format for each browser on its own. Fighting that is unnecessary and can actually produce a worse result.
So the real job is not "convert everything to WebP." The real job is:
- Give Shopify a good, high-quality source image to work from.
- Request it at the correct size for where it displays.
- Let it generate a proper
srcsetso each device gets an appropriately sized file. - Load above-the-fold images eagerly and everything else lazily.
- Reserve layout space so nothing shifts.
Almost all of that is done through the right Liquid filters, which we will get to a little later.
Step 1: Give Shopify a good source, and compress before upload
Shopify optimizes delivery, but it cannot un-bloat a badly prepared source file. Do not upload a 20MB, 6000px image straight off a camera or a stock site.
- Compress and right-size your master image before upload using a tool like Squoosh or TinyPNG (which handles JPEG and PNG despite the name).
- Upload at a sensible maximum: a source somewhere around 2048px on the long edge is plenty for almost any storefront use, since Shopify will generate call the smaller sizes you actually serve.
- Keep PNG only for graphics that genuinely need transparency or crisp edges (logos, icons, line art). Use JPEG or a high-quality source for photography and let the CDN convert.
A note on image-optimization apps: they can be useful, but remember every app adds its own script load to every page. If an app is mostly doing compression you could do once before upload, weigh that recurring performance cost against the convenience.
Step 2: Serve images with the right Liquid filters
This is where most real theme-level wins live, and where custom-built stores most often go wrong. Shopify gives you a small set of image filters that, used correctly, produce a properly sized, responsive, lazy-loaded <img> tag with almost no effort.
A quick but important note on modern versus deprecated filters. Shopify has two generations of image filters. Use the current ones, image_url and image_tag, and avoid the deprecated img_url and img_tag, which you will still see in a lot of old themes and outdated blog posts. The deprecated filters are exactly the kind of legacy code that quietly holds a store back.
image_url: request the image at a specific size
The image_url filter builds a CDN URL for an image at the width (and optionally height) you specify. This is how you avoid the classic mistake of shipping a 2000px file into a 400px slot.
{{ product.featured_image | image_url: width: 600 }}
You can constrain both dimensions and control cropping when the target aspect ratio differs from the source:
{{ product.featured_image | image_url: width: 800, height: 800, crop: 'center' }}
The crop parameter accepts top, center, bottom, left, and right, with center as the default. There is also a pad_color parameter (a hex value like 'ffffff') if you want to pad to an aspect ratio instead of cropping.
image_tag: generate a proper responsive <img>
On its own, image_url just gives you a URL. Pipe it into image_tag and Shopify generates a complete <img> element for you, and this is where a lot of the optimization comes for free:
{{ product.featured_image | image_url: width: 600 | image_tag }}
Two things happen automatically here that matter a great deal:
Width and height are set for you. By default image_tag adds width and height attributes derived from the image's dimensions and aspect ratio. That is exactly what prevents Cumulative Layout Shift: the browser reserves the correct space before the image arrives, so nothing jumps. You do not have to hand-calculate anything.
A srcset is generated for you. By default image_tag produces a srcset with a sensible set of widths up to the size you requested, so each device downloads an appropriately sized file rather than everyone getting the same large image. This is the single biggest bandwidth saving available, and you get it just by using the filter properly.
You can add attributes like a class:
{{ product.featured_image | image_url: width: 800 | image_tag: class: 'product__image' }}
Control the responsive widths and sizes explicitly
If you want to define the exact widths in the srcset rather than accept the defaults, pass widths as a comma-separated string:
{{ product.featured_image
| image_url: width: 1200
| image_tag: widths: '400, 600, 800, 1200' }}
Pair that with a sizes attribute so the browser knows how much space the image occupies at each breakpoint and can pick the smallest sufficient file. A product image that is full width on mobile but half width on desktop, for example:
{{ product.featured_image
| image_url: width: 1200
| image_tag:
widths: '400, 600, 800, 1200',
sizes: '(min-width: 750px) 50vw, 100vw' }}
Getting sizes right is one of the highest-value, most-overlooked pieces of image optimization, because without it the browser has to guess and often downloads more than it needs.
Step 3: Handle lazy loading correctly (and know the one exception)
Lazy loading defers below-the-fold images until the shopper scrolls toward them, so the initial paint only handles what is actually visible.
Here is the useful part: image_tag handles this intelligently on its own. Unless you apply a preload attribute, Shopify automatically sets loading to lazy for images further down the page. In other words, correct default behavior comes built in when you use the filter.
The critical exception is your above-the-fold hero or main product image. That image is almost certainly your LCP element, and it must not be lazy-loaded. You want it to load eagerly, and ideally preloaded. You should not lazy-load images above the fold. If your theme's default logic gets this wrong for a given template, Shopify exposes section.index and section.location so a developer can write correct above-the-fold logic rather than lazy-loading everything blindly.
This is exactly the kind of thing custom theme work frequently breaks. Either it strips the smart defaults and lazy-loads the hero (tanking LCP), or it removes lazy loading entirely (bloating the initial load). If your store has had custom development, this is worth checking explicitly, because it is one of the most common hidden LCP killers.
Step 4: Layout stability comes from letting the filters set dimensions
Cumulative Layout Shift, the annoying jump where you go to tap a button and it moves, is very often caused by images that do not reserve their space before loading.
The fix is mostly already covered above: because image_tag sets width and height attributes by default from the image's real aspect ratio, using it correctly protects your CLS almost automatically. Where stores get into trouble is hand-rolling raw <img> tags without dimensions, or overriding those attributes. If you build <img> markup manually, always set explicit dimensions or wrap the image in a container with a reserved aspect ratio. There is no cost to doing this and a real cost to skipping it.
Step 5: Handle video without wrecking performance
Video is powerful for conversion but dangerous for speed if handled naively. A self-hosted video file can dwarf every other asset on the page combined. Shopify gives you two Liquid filters that make this much safer.
Shopify-hosted video: video_tag
For video uploaded to Shopify (product media or files), use the video_tag filter rather than hand-writing a <video> element:
{% for media in product.media %}
{% if media.media_type == 'video' %}
{{ media | video_tag }}
{% endif %}
{% endfor %}
The reason this matters for performance. When you upload an MP4, Shopify also generates an HLS (m3u8) source alongside it. HLS lets the player adapt the stream to the visitor's connection quality rather than forcing everyone to download one heavy file, and the player falls back to the MP4 if HLS is not supported. You get adaptive streaming essentially for free by using the filter. One caveat worth knowing, if you enable loop, Shopify intentionally does not use the HLS source, so it can cache the video via progressive download instead.
External video (YouTube/Vimeo): external_video_tag
For video hosted on YouTube or Vimeo, use external_video_tag, which generates the embed iframe for you and lets you pass player options:
{% for media in product.media %}
{% if media.media_type == 'external_video' %}
{% if media.host == 'youtube' %}
{{ media | external_video_url: color: 'white' | external_video_tag }}
{% elsif media.host == 'vimeo' %}
{{ media | external_video_url: loop: '1', muted: '1' | external_video_tag }}
{% endif %}
{% endif %}
{% endfor %}
Offloading video to YouTube or Vimeo keeps the heavy delivery and transcoding off your critical path entirely.
General video rules
- Do not self-host large, unoptimized video on the storefront. Prefer Shopify-hosted video (for the automatic HLS benefit) or an external host.
- Lazy-load or facade your video. A "click to play" thumbnail that only loads the real player on interaction keeps the initial page light.
- Never autoplay heavy video above the fold. If you want motion in the hero, a short compressed muted clip is far lighter than a full player, and you should still test its effect on LCP.
- Replace animated GIFs with video. GIFs are enormous: a single two-second GIF can hold 20-plus frames, and two or three of them can add 10MB or more to a page. A short compressed MP4 is a fraction of the size at better quality.
A quick checklist
Before you consider your media optimized, verify:
- Source images compressed and sensibly sized before upload (no raw 20MB masters).
- Images served through
image_urlandimage_tag, not the deprecatedimg_url/img_tag. - Each image requested at an appropriate
widthfor where it displays, not the full source size. - A
srcsetpresent (theimage_tagdefault, or explicitwidths) plus a correctsizesattribute. widthandheightattributes present on every image so nothing shifts.- The above-the-fold hero loading eagerly (or preloaded); everything below lazy-loaded.
- Video served via
video_tag(Shopify-hosted, gets HLS) orexternal_video_tag(YouTube/Vimeo), never a heavy autoplaying self-hosted file. - No animated GIFs, short MP4s used where motion is needed.
Then re-run PageSpeed Insights on a product page, mobile first, and watch LCP and CLS both improve.
Reference
The Liquid filters covered here are documented on Shopify's developer site:
image_url: https://shopify.dev/docs/api/liquid/filters/image_urlimage_tag: https://shopify.dev/docs/api/liquid/filters/image_tagvideo_tag: https://shopify.dev/docs/api/liquid/filters/video_tagexternal_video_tag: https://shopify.dev/docs/api/liquid/filters/external_video_tag
The bottom line
Most image and video optimization on Shopify is really about using the platform correctly. Give the CDN a clean source, request the right sizes, and let the modern Liquid filters generate responsive, lazy-loaded, dimension-locked markup for you. The CDN handles WebP and AVIF negotiation on its own, so the work is sizing, srcset, sizes, and load order, not manual format conversion.
Where it gets genuinely technical is in the theme itself: correct sizes values per template, right handling of the above-the-fold exception, video facades, and untangling the deprecated filters and hand-rolled markup that older custom themes are full of. That is exactly the part DIY efforts tend to get wrong. If your images are dragging your store down and you would rather have the media pipeline built correctly in your theme and verified against your Core Web Vitals, that is a good place to bring in a specialist.