Image & Video Delivery in the Headless Frontend: A Practical Guide
Image & Video Delivery in the Headless Frontend: A Practical Guide
The decision on which CDN to use is usually already made by the time you're reading this. The real work is in the delivery: the right format per browser, breakpoint sets that match your actual layout, lazy-loading that doesn't hurt LCP, and measurement that proves it worked. This guide walks through that concretely for Shopware, commercetools, and Shopify. No vendor comparison, just implementation.
Format matrix: AVIF, WebP, JPEG fallback
Three formats, three roles:
- AVIF gives the best compression at comparable perceived quality, roughly 30 to 50 percent smaller than equivalent WebP (a rule-of-thumb, not a guarantee, it depends on image content). Supported in current Chrome, Firefox, Edge, and Safari from version 16.4.
- WebP is the broad fallback, supported for years across all relevant browsers, including Safari versions before 16.4.
- JPEG stays the last-resort fallback for email clients, PDF export tools, or older crawlers that parse neither AVIF nor WebP.
In practice that means a <picture> element with three sources, letting the browser pick:
<picture>
<source type="image/avif" srcset="/img/sneaker-p-400.avif 400w, /img/sneaker-p-800.avif 800w, /img/sneaker-p-1200.avif 1200w">
<source type="image/webp" srcset="/img/sneaker-p-400.webp 400w, /img/sneaker-p-800.webp 800w, /img/sneaker-p-1200.webp 1200w">
<img src="/img/sneaker-p-800.jpg" srcset="/img/sneaker-p-400.jpg 400w, /img/sneaker-p-800.jpg 800w, /img/sneaker-p-1200.jpg 1200w"
sizes="(min-width: 992px) 660px, 100vw" width="800" height="800" loading="lazy" decoding="async"
alt="Sneaker product detail, side view">
</picture>width/height are mandatory even when srcset serves variable widths. They reserve layout space and prevent CLS while the image loads.
srcset and sizes that match the real breakpoint
The usual copy-paste ladder (320, 640, 960, 1280, 1920) ignores how wide the image actually renders. Three frontends, three different answers:
- Shopware (Storefront theme built on Bootstrap, breakpoints at 576/768/992/1200/1400px): the PDP gallery renders full container width below 768px, and around 660px in a two-column layout above 992px. Sensible widths: 400, 660, 900, 1200, not the generic ladder.
- commercetools frontends (frequently Next.js with Tailwind default breakpoints, 640/768/1024/1280px): the PLP grid shows 2 columns on mobile, 3 on tablet, 4 from 1280px. With 4 columns and gutters, a single tile renders around 280px, roughly 340px on mobile. Widths should be 280, 340, 560 (the 2x variant for high-density displays).
- Shopify (Dawn/Hydrogen themes, typical max content width of 1600px): the category hero is full-bleed (
sizes="100vw"), but doesn't need to ship wider than 3200px even on 4K displays because the content width is hard-capped. Anything above that is wasted transfer.
Short version: sizes needs to describe the actual rendered width per breakpoint, not the viewport width. Set sizes="100vw" everywhere while the image sits in a single grid column, and you systematically over-serve.
For the implementation itself, https://www.laioutr.com/en/cloud/image-cdn is where width sets get defined per layout, instead of hard-coded in the template.
loading and fetchpriority: when lazy-loading hurts LCP
loading="lazy" has become the default, often applied globally inside a framework's shared image component. That's exactly the wrong setting for one image: the LCP candidate, usually the hero banner or the PDP main image.
Two common symptoms:
- The image component sets
loading="lazy"regardless of viewport position. The LCP image then only gets requested after the intersection check, not immediately as the initial HTML parses. - The image sits behind
data-srcinstead ofsrcbecause a JS lazy-loading library owns the resolution. The browser's preload scanner can't discover it early, even if it's above the fold.
For the LCP candidate:
<img src="/img/hero-1200.avif" fetchpriority="high" loading="eager" decoding="async"
width="1200" height="630" alt="Summer collection category hero">Add a <link rel="preload" as="image"> in the <head> if the asset only gets attached via CSS background or JS hydration. Everything below the first viewport stays loading="lazy", where it actually saves bytes.
Video: poster frames, preload strategy, adaptive bitrate
Video elements need the same above-the-fold discipline as images:
- Poster frame: a static AVIF/WebP still (extracted from the first second via
ffmpeg), reserving layout space and preventing CLS while the video loads. - Preload:
preload="none"as the default for below-the-fold video,preload="metadata"for above-the-fold video (loads only duration and dimensions, not the full stream),preload="auto"reserved for very short loop assets under 2 MB. - Adaptive bitrate: for hero videos, serve an HLS manifest (
.m3u8) with multiple renditions (e.g., 360p/720p/1080p) instead of a single fixed MP4. The player picks based on measured throughput, instead of forcing desktop-quality on every connection.
<video poster="/img/hero-poster.avif" preload="metadata" playsinline muted loop width="1200" height="630">
<source src="/video/hero.m3u8" type="application/x-mpegURL">
<source src="/video/hero-720p.mp4" type="video/mp4">
</video>As shown in core-web-vitals-image-video-delivery-lcp-storefront-2026, the poster frame is often the actual LCP candidate, not the video itself, until the stream is ready.
Measuring: LCP, CLS, transferred bytes
Three measurement points, three tools:
- LCP: field data via the
web-vitalsJS library (onLCPcallback) or Chrome UX Report. Google's thresholds: good at ≤ 2.5s (75th percentile), needs improvement up to 4s, poor above that. - CLS:
onCLScallback, good at ≤ 0.1. Reservedwidth/heighton every image and video element is the biggest lever against it. - Transferred bytes: Chrome DevTools Network panel filtered by
img/media, or the Lighthouse resource summary. A worked example: a PDP with 8 product images at roughly 120 KB each (AVIF, 800px width) instead of 8 x 350 KB (uncompressed JPEG at original size) drops image transfer from roughly 2.8 MB to roughly 960 KB. That's an illustrative calculation to show the order of magnitude, not a guaranteed number for every catalog.
The post what-is-an-image-cdn-2026 covers the image CDN fundamentals themselves; this guide picks up where the frontend implementation starts.
More from the Laioutr Platform
FAQ
Do I need to generate AVIF myself? No, an image CDN handles the format conversion on the fly based on the Accept header. The storefront only ships the source file once.
How many breakpoints do I actually need? Usually 3 to 5, matched to the theme's real CSS breakpoints, not to device classes.
Next steps
If you want to walk through the format matrix and breakpoint logic on your own storefront: Composable Headless Frontend shows how image and video CDN plug into the Laioutr frontend layer.
About the author: Marcel Thiesies is Co-Founder of Laioutr and owns product and architecture for the Frontend Management Platform.