lead
On most Magento category and product pages the Largest Contentful Paint element is an image, which makes Magento LCP images less a subtopic of performance than the whole of it for those templates. The engineering is well understood — newer formats, right-sized variants, defer what is off-screen. What actually decides whether a store still passes in eighteen months is something else: whether any human has to remember to do it. A pipeline that requires someone to export four sizes in three formats every time a product photo changes will be followed for a month and abandoned by the quarter.
So this is written around one constraint: the shop maintainer uploads one image and does nothing else. Everything below either happens automatically at the server or build layer, or it is not worth having.
That constraint splits the work cleanly in two, and so does the writing. This article covers where the LCP time actually goes and the format pipeline that needs no template changes at all. The template work it leaves out — declaring sizes in view.xml, emitting srcset, and preloading the LCP image — is a companion piece, Magento Responsive Images and Preload.
This is one of four deep dives under Magento 2 Core Web Vitals: From Fails to Passes, which carries the metrics, the level ordering and the cost-benefit framing.
The one rule that breaks more LCP scores than any other: never lazy-load the LCP image. loading="lazy" on a hero or first-row product image delays the exact paint the metric measures, and it is the most common self-inflicted regression in this entire area. Lazy-load below the fold, eagerly load and preload above it.
It is worth seeing how easily this happens, because it is not only a mistake other people make. This is a stock Magento 2.4.8-p5 Luma install in production mode, sample data, category page, mobile Lighthouse – nothing customised:

Core Magento renders the first product tile – the LCP element – with loading="lazy" and no fetchpriority. The default template lazy-loads every product image in the grid, including the ones above the fold, because the template cannot know where the viewport will cut off. So the fix is not "stop making this mistake"; it is knowing that the platform makes it for you, and overriding it for the first row.
baseline: how images actually cost you LCP and CLS
👥 For: Frontend, DevOps, Content team
⏱ TL;DR: Fix discovery first (never lazy-load the LCP image, add fetchpriority) → then right-size via view.xml and srcset → then convert formats automatically. On a stock install that is the order of payoff, and it is not the order most guides give.
Why images decide LCP (they are usually the LCP element)
On Magento category and product templates the Largest Contentful Paint element is almost always an image, which is what makes this worth a deep dive. What that does not automatically mean is that the fix is compression – and the measurement below is the reason to be careful about the difference.

That is the same category page, split into its four subparts. Two things are worth taking from it. The LCP element is a product image, as expected – but the download itself is only 50 ms of it. The dominant cost is the 420 ms resource load delay: the browser did not start fetching the image until late, which is exactly what loading="lazy" and a missing preload buy you. Compressing that image further would have moved LCP by almost nothing; discovering it earlier moves it by 420 ms.
A. Image Impact on LCP & CLS
Weight impact — and the claim you should stop repeating. Image optimisation guides, this one included until it was measured, tend to open with "images are 70–80% of page weight". On a stock Magento install that is simply not true, and it is worth seeing the actual split from the same two runs:
| Resource | Category page | Product page |
|---|---|---|
| Script | 618 KiB (67%) | 695 KiB (71%) |
| Image | 97 KiB (11%) | 79 KiB (8%) |
| Font | 73 KiB (8%) | 73 KiB (7%) |
| Stylesheet | 73 KiB (8%) | 77 KiB (8%) |
| Document + XHR + other | 60 KiB (7%) | 62 KiB (6%) |
| Total transfer | 921 KiB | 985 KiB |
Images are the smallest significant category on both pages. JavaScript outweighs them roughly seven to one.
The reason is view.xml: Magento never serves the merchant’s upload. A 4000-pixel, five-megabyte original is resized to the declared 240×300 or 700×700 and cached, so the bytes behind the scary numbers never reach the browser. The 70–80% figure survives only for content that bypasses the catalog image cache — page-builder banners, CMS blocks, theme heroes.
So why is this article worth reading at all? Because weight share and LCP responsibility are different questions. On both pages the LCP element is an image, and the metric is defined by when that one image paints. It does not matter that images are 11% of the bytes if the 11% contains the element being measured.
That also changes what "optimising" means here. With 420 ms in resource load delay against 50 ms of download, the wins rank: discovery first (stop lazy-loading the LCP image, add fetchpriority="high"), then right-sizing (700×700 into a 382×382 box wastes ~70% of that file), then format — worth having, free once automated, and the smallest of the three.
Format savings, measured rather than quoted. Re-encoding 40 cached catalog images at their existing dimensions with PHP’s GD — the same imagewebp() and imageavif() calls the optimizer module makes — cut 1311 KiB of JPEG to 849 KiB as WebP q80 (−35%) and 915 KiB as AVIF q75 (−30%). Note the ordering: at thumbnail sizes AVIF came out 8% larger than WebP, not 30% smaller as the usual advice has it. AVIF’s advantage is real on large photographic heroes and depends heavily on the encoder — measure your own catalog before assuming the newer format wins.
Layout impact (CLS):
- Images without width/height → browser doesn’t reserve space → layout shift when loaded
- Example: Product image loads after text → text jumps down (CLS hit)
- Solution: Always specify width/height (or aspect ratio in CSS)
B. Implementation Owner: Who Optimizes Images
| Task | Owner | Effort | Why This Owner |
|---|---|---|---|
| AVIF/WebP conversion | Backend | Hours | Module install and config; conversion is GD in a cron, not a separate service |
Accept negotiation rule | DevOps | Hours | One nginx or Apache block, plus making sure Vary: Accept survives the CDN |
Sizes in view.xml | Frontend | Hours | Declaring the widths the theme actually renders |
Responsive srcset + eager/lazy | Frontend | 1–2 days | A template override, in the two or three templates that render the LCP element |
| Content team training | Frontend | 1–2 hours | Explain that roles are pointers: one upload, no manual resizing |
| Testing (mobile/desktop) | Frontend + QA | 2–4 hours | Cross-device validation |
Coordination: DevOps builds pipeline → Backend integrates → Frontend updates templates → Content applies
tradeoff: every format and breakpoint you add is maintenance you own
🎯 Rule: Automate everything, manual work = maintenance debt
Pick the Right Tool (Least Maintenance)
| Approach | Maintenance | Setup | Pick If |
|---|---|---|---|
| BroCode Image Optimizer | ✓ Minimal (cron, or queue) | Hours — see below | You can manage nginx config + want zero template changes |
| Yireo Webp2 | ✓ Minimal (automatic <picture>) | 3 days | You need turnkey + no server config access + small catalog (<50k images) |
| JaJuMa Ultimate | ✓ Minimal (fully managed) | 2 days | You want responsive/retina/lazy-load turnkey (costs €199/year) |
| Manual conversion | ❌ High (per-image work) | N/A | Never do this at scale |
On that "hours": the BroCode setup cost is not one number, because the family is tiered and you only pay for the tier you take.
- Base + a format converter —
composer require, enable, set a quality, add the server rule. The nightly cron does the rest. That is an afternoon, and it is the whole thing for most stores. -queue— moves conversion onto Magento’s own MySQL queue. No new infrastructure; you need a consumer running, which any 2.4 install already does.-amqp— the same consumer against RabbitMQ. Also trivial if RabbitMQ is already in the stack, which on a production 2.4 it usually is. Standing one up purely for image conversion is the only version of this that costs real time, and it is rarely worth it.
→ TL;DR: Pick BroCode or Yireo. Never do manual cwebp on 1000s of images.
Configuration: Use Sensible Defaults
BroCode Image Optimizer (example automation):
# 1. Enable the base module and the converters for the formats you want
bin/magento module:enable BroCode_ImageOptimizer
bin/magento setup:upgrade
# 2. Configure once, under Stores > Configuration > Services >
# BroCode ImageOptimizer. Three config paths, that is the whole surface:
bin/magento config:set brocode_imageoptimizer/general/cron_enabled 1
bin/magento config:set brocode_imageoptimizer/webp/enabled 1
bin/magento config:set brocode_imageoptimizer/webp/quality 80
bin/magento config:set brocode_imageoptimizer/avif/enabled 1
bin/magento config:set brocode_imageoptimizer/avif/quality 75
# 3. The module ships its own nightly cron job (bro_code_image_scan, 00:00)
# that scans the configured paths and converts what is new. To force a
# pass by hand - for a first run over an existing catalog, say:
bin/magento images:optimize:scan
Result: the shop maintainer does nothing. New images are picked up by the nightly scan and converted in the background.
working_example: the optimizer module end to end
Be clear about the division of labour here, because it is the part most write-ups get wrong. The module does formats. It does not do responsive markup.
# 1. Base module plus one converter per format you want
composer require brocode/module-image-optimizer
composer require brocode/module-image-optimizer-webp
composer require brocode/module-image-optimizer-avif
bin/magento module:enable BroCode_ImageOptimizer
bin/magento setup:upgrade
# 2. Configure under Stores > Configuration > BroCode > Image Optimizer
# Base module: enable the cron that scans for new images
# Per format: enabled, and a quality setting
# 3. It scans the configured paths (pub/media and friends) and writes a
# sibling file per format next to each original:
# product.jpg (untouched original)
# product.webp
# product.avif
Delivery is a server rule, not a template change – which is the whole point of the design, and why installing it requires no .phtml edits:
# One file requested, best supported format served
location ~* \.(jpe?g|png)$ {
add_header Vary Accept;
try_files $uri$avif_suffix $uri$webp_suffix $uri =404;
}
What it deliberately does not do: generate breakpoint variants, write srcset or sizes, emit a <picture> element, or set loading and fetchpriority. Those are theme decisions, and they stay a one-off developer task in the handful of templates where the LCP element actually lives – the subject of the companion piece, Magento Responsive Images and Preload, which carries the view.xml sizing, the template override and the preload block. If you would rather have that part shipped for you, the optimizer comparison covers who does what.
What that is actually worth. Taking the measured product page and applying each step in turn — the byte figures are Lighthouse’s own estimates and the WebP figure is the 40-image benchmark from the baseline, so treat them as the shape rather than a promise:
| Step | Effect on the LCP image (23.6 KiB at 700×700) |
|---|---|
| Right-size to the 382×382 display box (measured: the stock product page serves 700×700 into it) | −16.5 KiB, roughly 70% |
| Convert what remains to WebP | a further ~35% of it |
Fix discovery (no loading="lazy", add fetchpriority) | no bytes at all — 420 ms on the category page |
The last row is the one to notice. It saves nothing and it is the largest single win, which is the whole argument of this article compressed into one line: on a stock Magento install the LCP problem is mostly a scheduling problem wearing an image costume.
verification: did the images actually help?
Three checks, in this order. Each answers a different failure.
# 1. Is the negotiated format actually being served?
curl -sI -H 'Accept: image/avif,image/webp,*/*' \
https://your-store.example/media/catalog/product/x/y/example.jpg | grep -i 'content-type\|vary'
# Expect: content-type: image/avif (or webp), and a Vary: Accept header
# 2. What is the browser actually downloading for this viewport?
# DevTools → Network → Img, sorted by size. A 1600px file on a 390px
# viewport means srcset/sizes is wrong, not that the format failed.
# 3. Is the LCP element the image you think it is?
// Paste in the console: reports the real LCP element and when it painted
new PerformanceObserver((list) => {
const e = list.getEntries().at(-1);
console.log('LCP', Math.round(e.startTime), 'ms —', e.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });
That last one is the check worth running first. Teams routinely optimise a hero banner for a week before discovering the LCP element was a category description block rendering a webfont, and no amount of image work was ever going to move it.
Pass criteria: AVIF or WebP served to browsers that accept it, no image transferred at more than roughly twice its displayed CSS pixel width, the LCP image neither lazy-loaded nor discovered late, and no layout shift attributable to an image (every <img> carries width and height or an aspect-ratio box).
Related reading
- Magento Responsive Images and Preload — the template half of this article:
view.xmlsizing,srcsetviagetImage()‘s attributes argument, and preloading the LCP image properly. Publishes three days after this one, on 21 September 2026. - Magento 2 Core Web Vitals: From Fails to Passes — the pillar this deep dive sits under.
- BroCode Image Optimizer — WebP & AVIF for Magento 2 — the module that makes the conversion automatic.
- AVIF vs WebP for Magento: Measure Your Own Catalog — the format decision, benchmarked on real catalogs.
- Converting a Large Catalog Without Melting the Server — the bulk backfill, queued.
- No Template Changes: Serving WebP and AVIF with nginx — content negotiation at the server layer.
- Rolling Out AVIF Without Breaking Your CDN — the
Vary: Accepttrap. - Magento Image Optimizer: brocode vs Yireo vs JaJuMa — if you would rather not build it.
Sources & References
- MDN: responsive images —
srcset,sizesand the<picture>element. - web.dev: optimize LCP — the four phases of LCP and where images sit in them.
- web.dev: browser-level lazy loading — including why the LCP image is the exception.
- Can I use: AVIF — current support, which is why the fallback chain still matters.
Disclaimer
File-size figures depend entirely on image content — photographic product shots compress very differently from flat graphics with large single-colour areas. Measure your own catalog rather than adopting a ratio from any article, this one included.