In short and in plain language
A product page often has many images. The first big image is usually the most important one. It should load at once.
The other images load after that. The small images in the strip should be small files too.
The big image for zooming loads only when someone clicks or points at the image. This keeps the page fast at the start.
A video first shows just one image. The video loads only when someone starts it.
We check your product page. Then we set up the images so that they load in the right order.
On a product page, the images carry a large part of the information: material, finish and variants frequently only become clear through the gallery and zoom. Yet these very images often make the page slow: galleries load every view at once, fetch full-resolution zoom files during the page load or, of all things, set the main image to lazy. This article sorts out which image should load when: the first gallery image right away and with high priority, further images and the thumbnail strip afterwards, the zoom file only on interaction. On top of that come image sizes per template, a variant switch that does not reload the whole gallery, and placeholders for videos and 360-degree views. Where Shopware is concerned, we rely on the source code of the storefront in version 6.7.
Key takeaways
- On 76 percent of mobile pages, the LCP element is an image (Web Almanac 2025). On a product page, this is usually the first gallery image: it loads eagerly, sits in the HTML with
srcand carriesfetchpriority="high". - Further images in the gallery slider get
loading="lazy"andfetchpriority="low". Since Chrome 121, horizontal carousels use the same distance thresholds as scrolling (web.dev), so lazy alone does not hold them back. - The zoom file is only referenced as a URL in an attribute and loads on click, hover or focus. It is better to deliver the largest thumbnail size instead of the original, and in the zoom window the clicked image loads first.
- Image sizes are defined per template: by default, Shopware creates sizes of 400, 800 and 1920 pixels for product media (Shopware source code). Thumbnails take the smallest size, the magnifier the largest.
- In the Shopware storefront, a variant switch loads the page of the new variant completely (Shopware source code). If you only swap the gallery, keep its container at a fixed size and preload the variant main image when the selection is pointed at.
- Videos and 360-degree views start as placeholders with a poster image and
preload="none". So far, only Chrome and Edge from version 150 supportloading="lazy"for videos (MDN browser-compat-data).
Why the first gallery image matters on a product page
Largest Contentful Paint measures when the largest visible element in the first screen has finished rendering. On 85.3 percent of desktop pages and 76 percent of mobile pages, that element is an image (Web Almanac 2025). On a product page, it is usually the first image of the gallery: it sits at the top, takes up almost the full width on a smartphone and is larger than the product name, price and buy button. How fast this one image appears therefore determines the LCP of the whole template. A good value is 2.5 seconds or less at the 75th percentile of page loads.
A gallery rarely consists of a single image, though. There are further views in the slider, a strip of thumbnails, large files for the magnifier or full screen, often a product video and sometimes a 360-degree view. All these files compete for the same connection in the first seconds. We regularly see three patterns in shop audits: the gallery loads all images at once, it fetches the zoom files during the page load, or it lazy-loads the main image, of all things, via loading="lazy" or via a script and data-src. The third pattern is common: about 16 to 17 percent of pages lazy-load their LCP image, a share that has barely moved since 2024 (Web Almanac 2025).
Scope
The main image: eager, discovered early, high priority
For the first gallery image, the basic rule of lazy loading applies in reverse. Images that are likely to be in the viewport when the page loads, and LCP images in particular, are not lazy-loaded, as the web.dev guide puts it. The reason follows right there: the browser can only load a lazy image once it knows where the image sits on the page, that is, after layout. An analysis of HTTP Archive and CrUX data gave an idea of the magnitude: the median page without lazy loading had a 75th percentile LCP of 2,922 milliseconds, the median page with lazy loading 3,546 milliseconds (web.dev 2021). The authors themselves stress that this is a correlation, not proof that lazy loading causes the delay.
Use loading=lazy only for images outside the initial viewport.
Eager alone is not enough. The image has to be in the HTML so that the browser preload scanner finds it before CSS and JavaScript have loaded. An <img> with src and srcset in the delivered document does that. An image that a gallery script inserts later, a URL in data-src or a CSS background do not. As a guideline for a well-optimized page, the time between the first byte and the start of the LCP download, the resource load delay, should stay below 10 percent of LCP (web.dev). How to measure these subparts is shown in the article on the four phases of LCP.
That leaves priority. Chrome starts images at low priority and only boosts visible images after layout. Since Chrome 117, the first five large images of a page get Medium priority (web.dev). On a product page, these are often exactly the first five gallery images, and the main image shares the connection with four views nobody sees yet. fetchpriority="high" on the first image resolves this. The attribute now appears on 17 percent of mobile pages with an LCP image (Web Almanac 2025). Why exactly one element gets high priority and not several is explained in the article on fetchpriority and priority hints.
<!-- Main image: in the HTML, eager, high priority -->
<div class="product-gallery" style="aspect-ratio: 1 / 1">
<!-- Zoom file only as a URL in the link, link reachable by keyboard -->
<a class="gallery-zoom-link" href="/media/lamp-front-1920.webp"
aria-label="Zoom front view">
<img class="gallery-main"
src="/media/lamp-front-800.webp"
srcset="/media/lamp-front-400.webp 400w,
/media/lamp-front-800.webp 800w,
/media/lamp-front-1920.webp 1920w"
sizes="(min-width: 1200px) 600px, 100vw"
width="800" height="800"
fetchpriority="high"
alt="Table lamp with a blue shade, front view">
</a>
<!-- Further images: lazy and low priority -->
<img class="gallery-slide"
src="/media/lamp-side-800.webp"
srcset="/media/lamp-side-400.webp 400w,
/media/lamp-side-800.webp 800w"
sizes="(min-width: 1200px) 600px, 100vw"
width="800" height="800"
loading="lazy" fetchpriority="low"
alt="Table lamp, side view">
</div>
<!-- Thumbnail strip: small image size, fixed dimensions -->
<button type="button" class="gallery-thumb" aria-label="Show side view">
<img src="/media/lamp-side-400.webp" width="80" height="80" alt="">
</button>The Shopware storefront already provides the basis for this. The gallery element loads the first image without lazy loading and sets loading="lazy" on all further images (Shopware source code). The first image only gets fetchpriority="high", however, if the switch “Load first image with higher priority” is turned on in the gallery element of the product page layout, and in the element default settings it is off (Shopware source code). If you maintain layouts in Shopping Experiences, check this switch in every product page layout. Further levers for Shopware shops are described on our Shopware performance page.
Further images and thumbnail strip: later and smaller
The remaining views in the gallery slider get loading="lazy". If you expect them to load only on swipe, you are in for a surprise. Since Chrome 121, horizontal-scrolling images such as carousels use the same distance thresholds as vertical scrolling (web.dev). These thresholds are 1,250 pixels on fast connections and 2,500 pixels on slow connections from the viewport (web.dev). The next images in a slider are usually within that distance. So they load anyway, just after layout instead of immediately. For visitors this is pleasant, since the next image is usually there when they swipe. The price: the downloads fall into the page load.
This is why further images also get fetchpriority="low". web.dev explicitly recommends this for carousels: images just outside the viewport may count as close enough, get boosted to high priority and load even with loading="lazy"; fetchpriority="low" keeps them behind the first image. Watch out for the reverse mistake: on 0.3 percent of pages, the LCP image carries fetchpriority="low", likely unintentionally according to the Web Almanac (Web Almanac 2025). In galleries, this happens when a script treats all slides alike and does not exempt the first one.
A second pitfall is hidden image sets. Chrome, Safari and Firefox do not load lazy-loaded images with display: none, but they do load lazy-loaded images hidden with opacity: 0 (web.dev). A gallery that delivers two image sets for mobile and desktop and merely makes one of them transparent loads both.
On desktop, the thumbnail strip is usually visible in the first screen. Lazy loading does little there, since the thumbnails are in the viewport anyway. The lever is file size: a thumbnail is requested at the width it is displayed at, not at the width of the main image. Shopware requests the thumbnails of the strip with a display width of 200 pixels (Shopware source code) and deliberately omits loading="lazy" there. The reason is a comment in the template: in the layout with the strip on the left, the slider waits until every image has loaded, and browsers that defer lazy images would block its start (Shopware source code).
- Fixed dimensions:
widthandheighton every thumbnail so that the strip does not jump while loading. The median web page contains two images without size attributes (Web Almanac 2025). - Smallest image size: for a narrow strip, the smallest available size is enough. If you show very small thumbnails, add a separate, smaller size instead of scaling down files meant for the main image.
-
sizes="auto"for lazy images: for lazy-loaded images, the browser may take the display width from the layout instead of reading it from asizeslist. Chrome supports this from version 126, Firefox from 150 and Safari from 27 with limitations (MDN browser-compat-data). - Thumbnails as buttons: each thumbnail is a
<button>with an image inside and a label, not an image with a click handler. This keeps the strip keyboard-accessible and costs no load time.
Zoom: the large file only on interaction
In many shops, zoom is part of the product page’s basic equipment. The file for it is usually the largest in the gallery. If it loads with the page, it competes with the LCP image for the connection, and that for a feature only some visitors use. If the gallery works with reduced images on mobile, zoom needs a higher-resolution version; otherwise it only magnifies the blur.
Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed.
The pattern is simple. The HTML only contains the URL of the zoom file, for example in a data attribute or in the href of a link around the main image. It loads on the first interaction: on click or tap, on mouse hover and on keyboard focus. Hover and focus give it a head start, because some time passes between pointing at the image and clicking, and the download can already use it. So that the click still responds immediately, the zoom view opens with the main image that has already loaded and swaps in the large file once it is decoded.
// Load the zoom file on the first interaction only
const link = document.querySelector('.gallery-zoom-link');
const main = link.querySelector('.gallery-main');
let zoomReady = null;
function preloadZoom() {
if (zoomReady) return zoomReady;
const img = new Image();
img.fetchPriority = 'high';
img.src = link.href;
zoomReady = img.decode().then(() => img);
return zoomReady;
}
// Head start: mouse pointer or keyboard focus
link.addEventListener('pointerenter', preloadZoom, { once: true });
link.addEventListener('focus', preloadZoom, { once: true });
link.addEventListener('click', async (event) => {
event.preventDefault();
openZoomView(main.currentSrc); // open right away with the loaded image
const large = await preloadZoom();
showZoomImage(large.src); // swap in the large file once decoded
});This keeps the interaction fast as well. Interaction to Next Paint rates the time from an input to the next rendered frame; 200 milliseconds or less counts as good. A click handler that waits for a large file before painting anything is the wrong structure for that. More on interaction time in the article Optimizing INP.
Shopware follows this principle in two parts. On desktop, the magnifier only sets the file from the data-full-image attribute as a background when the mouse moves over the image (Shopware source code). That attribute, however, holds the URL of the original, the uploaded file, not of a thumbnail size. If you upload product photos straight from the camera, the magnifier receives files at full camera resolution. The full-screen zoom window also loads its images only when it opens and appears once all images of the window have loaded, commented in the code as “load images before modal is shown” (Shopware source code). With many product images, the click therefore waits for every file of the gallery. Both can be changed in the template: point the magnifier to the largest thumbnail size and load the clicked image first in the zoom window.
For search, little changes. Google finds images via the src attribute of <img> elements and does not index CSS images (Google Search Central). The main image stays a regular <img> with srcset, and the large version can additionally be listed in an image sitemap.
Define image sizes per template
Which file an image needs depends on where it appears, not on the product. The same photo shows up as a tile in the listing, as the main image on the product page, as a thumbnail in the strip and at full size in the magnifier. By default, Shopware creates thumbnail sizes of 400, 800 and 1920 pixels on the long edge for product media (Shopware source code). Via sizes, the gallery element tells the browser the full viewport width up to medium screens, then 60 and 50 percent of the width and a fixed 800 pixels on very wide screens (Shopware source code). From display width and pixel density, the browser picks the matching size; on a wide desktop with double pixel density, that is the 1920 size.
This is why a small overview per template pays off: which slot, which display width, which size, which loading behavior. For the product page gallery, it looks like this:
| Element | Loading | Priority | Image size | Timing |
|---|---|---|---|---|
| Main image (first gallery image) | eager, with src and srcset in the HTML | fetchpriority="high" | matching sizes and pixel density | right away with the document |
| Further images in the slider | loading="lazy" | fetchpriority="low" | same as the main image | after layout, before swiping |
| Thumbnail strip | with width and height | default | smallest size | with the page |
| Zoom file | URL in an attribute only | high, once requested | largest thumbnail size instead of the original | on hover, focus or click |
| Product video | poster image, preload="none" | poster like a further image | poster at gallery size | video only on playback |
| 360-degree view | placeholder image with a start button | placeholder like a further image | single frames at gallery size | only after the start |
All gallery images need the same aspect ratio, and the container gets it via aspect-ratio or fixed dimensions. Otherwise the content below jumps as soon as an image with a different format loads. How stable shop pages are built is described in Avoiding cumulative layout shift. Generating the sizes in WebP or AVIF is part of the image optimization we take on for shops.
Variant switch without reloading the whole gallery
When someone picks a different color, the image usually changes too. In the Shopware storefront, the product page requests the URL of the selected variant and then opens it as a new page (Shopware source code). For the browser, this is a full navigation: new document, new gallery, new LCP. If the page is in the HTTP cache, this is fast. If not, the visitor waits for the server, right in the middle of a buying decision.
The alternative is a partial switch: the page stays, and only gallery, price and availability swap their content. For the gallery, this means:
- Keep the gallery container at a fixed size so that nothing jumps during the swap.
- When a color option is pointed at or focused, preload the main image of that variant with high priority.
- On the switch, only swap
srcandsrcsetof the main image and load the further images of the variant lazily and at low priority again. - Leave images shared by all variants, such as detail shots or dimension drawings, in place instead of reloading them.
- Set the variant URL via the History API so that links, the back button and sharing keep working.
A partial switch has consequences for measurement. The new content is not a new page, so the browser does not measure a separate LCP for it. Shops that navigate this way should know how soft navigations are measured; this is covered in the article Soft navigations in CrUX. Whether the rebuild pays off is a matter of measurement: how often do visitors switch variants, and how long does the switch take today?
Videos and 360-degree views as placeholders
For the page load, a product video in the gallery is initially just an image, namely the poster image. With preload="none", the browser preloads nothing of the video itself; MDN sums up the value as “Indicates that the video should not be preloaded”. Treat the poster image like a further gallery image. loading="lazy" now exists for videos as well, but so far only Chrome and Edge support it from version 150, Firefox and Safari do not (MDN browser-compat-data). Poster image and preload="none" therefore remain the cross-browser approach; preload is a hint to the browser, not a binding instruction.
Embedded players from external platforms load their own script and files on embedding, before anyone presses play. The facade pattern addresses this: a static element looks like the player and is replaced by it only on click. The Lighthouse documentation describes the sequence from the placeholder on load via connecting on mouseover to the swap on click (Chrome for Developers). More on videos in shops in the article Video performance.
A 360-degree view usually consists of a series of single shots around the product or of a 3D model. Neither belongs in the page load. In its place, the gallery shows a still image with a clearly labeled start button, and only the click loads the image series or the model, with visible progress. The still image has the dimensions of the other gallery images so that nothing jumps on start. For visitors who never start the view, it costs a single image.
Checking: LCP element and waterfall of the product page
Whether the gallery loads correctly shows in the browser developer tools. The Performance panel shows which element the browser counted as LCP and how the time splits across the four subparts. In the network waterfall, you can filter for images: the main image should start right after the document, the further images afterwards at low priority, and the zoom file only appears after the first click on the image.
Lab measurements show the behavior, field data shows the effect. Product pages are secondary pages, and there, interaction on mobile lags clearly behind: on mobile, 80 percent of home pages reach a good INP, but only 69 percent of secondary pages (Web Almanac 2025). Gallery scripts, magnifiers and variant selection are typical candidates for this. So evaluate the product template separately, as described in Template performance beyond the home page, and check LCP, CLS and INP at the 75th percentile.
- The LCP element of the product page is the first gallery image and sits in the HTML with
srcorsrcset. - The first image carries no
loading="lazy"butfetchpriority="high", and no other image on the page carrieshigh. - Further images in the slider carry
loading="lazy"andfetchpriority="low". - Thumbnails are small image sizes with
widthandheight. - The zoom file is missing from the waterfall until the first interaction and is a thumbnail size, not the original.
- Videos have a poster image and
preload="none", 360-degree views a placeholder. - A variant switch leaves the gallery at its size.
In an analysis, we go through exactly this list for your product template, with lab values for desktop and mobile and, where available, CrUX field data for your product pages. The result is an order of changes by impact, not a flat-rate package. How we proceed is described under technical analysis; our page on Core Web Vitals explains the basics of the metrics. For shops with large product ranges, a look at PageSpeed in e-commerce is also worthwhile.
Sources and documentation
data-src, the preload scanner does not find it. Then the image belongs in the HTML as an <img>, which is the better approach, or it is preloaded with <link rel="preload"> at high priority. More on this in the article on fetchpriority.loading="lazy" and fetchpriority="low", they load after the main image instead of at the same time. Images far back in a long gallery only load once the slider gets close to them.src of <img> elements and does not index CSS images (Google Search Central). The main image therefore stays a regular <img> with srcset that also offers a high-resolution version in its largest size. A URL that only sits in a data attribute can additionally be listed in an image sitemap.Related Articles
Fast Shop Listing Filters: Facets Without the Wait
Every filter click in a shop counts toward INP. Confirm the click at once, compute facet counts sparingly, cache results and rebuild the listing without shifts.
Soft Navigations in CrUX: What SPA Shops Check by May 2027
Since October 2026, CrUX has shown Core Web Vitals with soft navigations. Who is affected, how LCP, INP and CLS shift and what SPA shops should do by May 2027.
Sale Launch at Midnight: Warm the Cache Instead of Waiting
At sale launch, prices change and the cache goes cold. Build a warm-up list from field data and the sitemap, plan the night and measure hits before release.