Images are the single largest payload on most Magento 2 storefronts. A typical product listing page ships 2-4 MB of images, a category page with 36 products easily crosses 10 MB, and PDP galleries can push even higher. When Google's Core Web Vitals penalize you for LCP above 2.5 seconds, every kilobyte matters.
The good news: Magento 2 has built-in tooling for most of this. The bad news: almost nobody configures it properly. Let's fix that.
The Cost of Unoptimized Images
Before diving into solutions, let's quantify the problem. A default Magento 2 install with the Luma theme loads:
- Product thumbnails: ~50-80 KB each (JPEG, no compression tuning)
- Category banners: 200-500 KB
- PDP main images: 150-300 KB
- Product gallery thumbnails: 30-50 KB each
A category page with 36 products at 70 KB per thumbnail = 2.5 MB just in thumbnails. Serve that over a 4G connection at 10 Mbps and you're looking at 2 seconds of pure image transfer — before rendering, layout, or JavaScript execution.
Now compare with the same page serving WebP at 40 KB per thumbnail: 1.4 MB. That's a 44% reduction with zero visible quality loss.
WebP: The Biggest Win With The Least Effort
Native Magento 2 WebP Support
Since Magento 2.4.4, the platform includes native WebP support through the Magento_Catalog module. When enabled, Magento automatically generates WebP variants alongside JPEG/PNG originals.
To enable it in view.xml (located in your theme's etc/ directory):
<?xml version="1.0"?>
<view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
<media>
<images>
<image id="product_page_image_medium">
<type>webp</type>
<width>700</width>
<height>700</height>
</image>
<image id="category_page_grid">
<type>webp</type>
<width>300</width>
<height>300</height>
</image>
</images>
</media>
</view>
After changing view.xml, regenerate images:
bin/magento catalog:images:resize
The <picture> Element Strategy
Magento's native WebP uses the <picture> element with <source> tags, which gives you progressive enhancement — browsers that support WebP get it, others fall back to JPEG:
<picture>
<source srcset="/media/catalog/product/image.webp" type="image/webp">
<source srcset="/media/catalog/product/image.jpg" type="image/jpeg">
<img src="/media/catalog/product/image.jpg" alt="Product name" loading="lazy">
</picture>
This is the gold standard. The browser negotiates format automatically. No JavaScript polyfills needed.
When Native Isn't Enough
The built-in WebP support has limitations:
- No AVIF generation — AVIF offers 20-30% better compression than WebP
- No quality tuning per breakpoint — you get one quality setting for all sizes
-
No automatic re-generation — changing settings requires manual
catalog:images:resize
For stores that need more control, a dedicated image optimization tool like hyva-themes/magento2-default-theme (which includes optimized image handling) or a CDN-based solution (Cloudflare Images, Imagekit, imgix) gives you per-breakpoint quality control and automatic AVIF.
Lazy Loading: Free Performance With One Attribute
Native Lazy Loading
Magento 2.4.6+ ships with native loading="lazy" on product images. If you're on an older version, you can add it via a small layout override in your theme's Magento_Catalog/templates/product/list.phtml:
<img src="<?= $escaper->escapeUrl($imageHelper->getUrl($_product, 'category_page_grid')) ?>"
alt="<?= $escaper->escapeHtml($_product->getName()) ?>"
loading="lazy"
width="300"
height="300" />
The width and height attributes are critical — they prevent Cumulative Layout Shift (CLS) by letting the browser reserve space before the image loads.
What About Above-the-Fold?
Lazy loading above-the-fold images is counterproductive — it delays LCP. The main product image on a PDP and the first row of category thumbnails should use loading="eager" (or simply omit the attribute):
<!-- First product in grid: eager -->
<img src="..." loading="eager" width="300" height="300" fetchpriority="high">
<!-- Rest: lazy -->
<img src="..." loading="lazy" width="300" height="300">
The fetchpriority="high" hint tells the browser to prioritize this image's fetch, which can shave 200-500ms off LCP on PDPs.
LQIP: Low-Quality Image Placeholders
For the ultimate perceived-performance win, use Low-Quality Image Placeholders (LQIP). The idea: render a tiny (20x20px), heavily compressed base64 image inline, then swap it with the real image when it loads:
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
data-src="/media/catalog/product/full-size.webp"
alt="Product"
class="lazyload"
width="300"
height="300">
A 20x20 JPEG base64 is ~300 bytes. It renders instantly, giving the user a blurred preview while the full image streams in. The visual effect is similar to Medium.com's image loading pattern.
Responsive Images: srcset Done Right
Magento generates multiple image sizes per product, but most themes only reference the largest one and let CSS scale it down. This means a mobile user downloading a 700x700 image that's displayed at 150x150.
Using srcset in Magento
The srcset attribute lets the browser pick the right size:
<img src="/media/catalog/product/700x700.webp"
srcset="
/media/catalog/product/150x150.webp 150w,
/media/catalog/product/300x300.webp 300w,
/media/catalog/product/500x500.webp 500w,
/media/catalog/product/700x700.webp 700w
"
sizes="(max-width: 768px) 150px, (max-width: 1200px) 300px, 500px"
alt="Product name"
loading="lazy">
The sizes attribute is where most implementations go wrong. It must match your actual CSS layout breakpoints. Measure the rendered image width at different viewport sizes using DevTools, then set sizes accordingly.
A wrong sizes attribute is worse than no srcset — the browser downloads the wrong image.
Defining Image Roles in view.xml
Magento's view.xml is where you define the image sizes that feed into srcset. Each image "role" (product_page_image_medium, category_page_grid, etc.) can have multiple variants:
<image id="category_page_grid">
<type>webp</type>
<width>300</width>
<height>300</height>
</image>
<image id="category_page_grid_small">
<type>webp</type>
<width>150</width>
<height>150</height>
</image>
Reference these in your template to build the srcset string dynamically.
CDN-Level Optimization
If you're using Varnish + a CDN (Cloudflare, Fastly, BunnyCDN), you can push image optimization to the edge instead of doing it in Magento.
Cloudflare Images
Cloudflare Images handles resizing, format negotiation, and compression at the edge. You serve one high-quality original, and Cloudflare generates variants on-the-fly:
<img src="https://example.com/cdn-cgi/image/width=300,format=webp,quality=80/media/catalog/product/image.jpg"
alt="Product">
Pros:
- No Magento image generation needed
- Automatic AVIF/WebP negotiation per browser
- Per-request resizing (no pre-generation step)
Cons:
- $5/month per 100,000 unique images
- Adds a dependency on Cloudflare's edge for image delivery
Fastly Image Optimizer
If you're on Magento Cloud, Fastly's built-in Image Optimizer is already available. Enable it in the Fastly configuration panel and configure quality/format settings. Fastly does on-the-fly WebP/AVIF conversion with no code changes.
BunnyCDN
BunnyCDN's Image Optimization is cheaper ($0.005 per 1,000 optimizations) and supports WebP, AVIF, and on-the-fly resizing. The URL structure:
https://bunnyoptimizer.example.com/media/catalog/product/image.jpg?width=300&output=webp&quality=80
JPEG Quality Tuning
Magento's default JPEG quality is 80, which is too high for web thumbnails. Most product images look identical at quality 65-70, and the file size difference is significant:
| Quality | File Size (300x300) | Visual Difference |
|---|---|---|
| 80 (default) | 48 KB | Baseline |
| 70 | 34 KB | Imperceptible |
| 65 | 29 KB | Barely noticeable on close inspection |
| 60 | 24 KB | Noticeable on detailed images |
To change the quality, modify your theme's view.xml:
<image id="category_page_grid">
<type>webp</type>
<width>300</width>
<height>300</height>
<quality>70</quality>
</image>
For WebP specifically, quality 70 is the sweet spot. WebP at quality 70 looks as good as JPEG at quality 80, with 30% smaller files.
Caching Optimized Images
Magento stores generated images in pub/media/catalog/product/cache/. These are served statically by Nginx/Varnish, so they're fast — but the first request for each unique size triggers generation, which is slow.
Pre-Warm the Image Cache
After deploying changes to view.xml, pre-generate all images:
bin/magento catalog:images:resize
This is a heavy operation on large catalogs (50K+ products can take 30+ minutes). Run it post-deploy, not during traffic spikes.
Varnish Caching for Images
Ensure your Varnish VCL caches image responses with long TTLs:
sub rule_image {
if (req.url ~ "\.(webp|jpg|jpeg|png|gif|svg)$") {
unset req.http.Cookie;
set req.url = regsub(req.url, "\?.*$", "");
return (hash);
}
}
sub vcl_backend_response {
if (bereq.url ~ "\.(webp|jpg|jpeg|png|gif|svg)$") {
set beresp.ttl = 30d;
set beresp.http.Cache-Control = "public, max-age=2592000, immutable";
}
}
The immutable flag tells browsers they never need to revalidate this URL — a significant win for repeat visitors.
Monitoring Image Performance
Lighthouse + WebPageTest
Run Lighthouse audits on category and PDP pages. Focus on:
- LCP element: Is it an image? If so, it must load in <2.5s
- Total image weight: Should be <1 MB for category pages, <500 KB for PDPs
-
CLS: Should be <0.1 — proper
width/heightattributes prevent this - Unused bytes in images: Are you serving 700x700 images in 150x150 slots?
Real User Monitoring (RUM)
Synthetic tests don't capture real-world conditions. Use RUM (Sentry, New Relic Browser, or a custom solution) to track actual LCP percentiles across devices:
// Track LCP for image elements
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry.element && lastEntry.element.tagName === 'IMG') {
navigator.sendBeacon('/rum/lcp', JSON.stringify({
lcp: lastEntry.startTime,
url: window.location.href,
imgSrc: lastEntry.element.src,
connection: navigator.connection?.effectiveType
}));
}
}).observe({ type: 'largest-contentful-paint', buffered: true });
This tells you which images are actually causing LCP issues for real users on real connections.
The Optimization Checklist
Before you ship, verify each item:
-
WebP enabled in
view.xmlfor all image roles -
Native lazy loading on all below-the-fold images (
loading="lazy") -
Eager loading +
fetchpriority="high"on LCP images -
widthandheightattributes on every<img>to prevent CLS -
srcsetwith correctsizesmatching your layout breakpoints - JPEG quality at 70 or lower for thumbnails
- Varnish caching for static images with 30-day TTL
-
Image cache pre-warmed after every
view.xmlchange - CDN-level format negotiation (AVIF > WebP > JPEG) if using Cloudflare/Fastly/Bunny
- RUM monitoring on LCP for image-heavy pages
Conclusion
Image optimization is the highest-ROI performance work you can do on a Magento 2 store. Unlike JavaScript bundling or database optimization — which require deep platform knowledge and carry regression risk — image optimization is mostly configuration. The difference between a default Magento install and a properly configured one is often 3-5 MB per pageview, which translates directly into better Core Web Vitals, higher conversion rates, and lower bounce rates.
Start with WebP and lazy loading. Those two changes alone will cut your image payload by 40-50%. Then layer on srcset, quality tuning, and CDN-level optimization to squeeze out the rest.
Top comments (0)