The problem: PHP in the image path
Magento is slow whenever a PHP process sits in the request path; a plain file transfer from disk is far cheaper. The whole design of this module family is built around that gap. It splits the work into two halves that never block each other:
- Conversion happens inside Magento. It decides which files need a modern format and produces them.
- Serving happens entirely at the web-server layer. Apache or nginx picks the right file based on the browser’s
Acceptheader — no PHP, no Magento, just a direct file read.
The bridge between the two halves is a sidecar file: each converted image is written next to the original with an added suffix (photo.jpg → photo.jpg.webp). The shop’s HTML never changes — it keeps pointing at photo.jpg — and the web server transparently upgrades the response when the client supports the newer format. Adding a format requires no template or markup changes anywhere in the storefront.
Impact Callout
Typical impact: catalog image weight cut 60–80 % for WebP and 70–90 % for AVIF — zero template changes, zero storefront inventory, works transparently across third-party modules and email content.
CTA Bar
View on GitHub — All Modules — Buy me a coffee ☕
The requester chooses the format, not the system
A subtle but important property: the client decides what it receives, not the server and not Magento. This is plain HTTP content negotiation. Every browser advertises what it can render in the Accept request header, and the web server simply honors that declaration by serving the best sidecar that both exists and is accepted. The system never guesses at, sniffs, or stores anything about the visitor’s capabilities.
The practical payoff is graceful degradation for free:
- A modern browser sends
Accept: image/avif,image/webp,…and gets the AVIF or WebP sidecar. - An older browser (or a bot, or
curl) that doesn’t advertise those formats never matches the rewrite condition and transparently receives the original JPEG/PNG. - No user-agent sniffing, no capability database, no JavaScript feature detection, and nothing to keep up to date as browser support changes — the requester is the single source of truth about what it can display.
So the same URL safely serves the right thing to every client simultaneously, and a visitor on a decade-old device is never handed a format it can’t decode.
Nothing in the application layer has to change
Because the swap happens on the file path keyed only by existence on disk and the Accept header, the application never participates in choosing the format. This leads to a concrete, often-underrated benefit: you don’t have to touch — or even audit — any templates. Storefront .phtml, layout XML, email templates, and (critically) third-party module templates all keep emitting <img src=".../photo.jpg"> and are silently upgraded. There’s no per-template integration, no list of "image outputs we still need to convert," and no risk that some vendor module rendering its own product/banner image gets missed. The optimization is invisible above the web server, so the surface area you have to reason about stays tiny no matter how many extensions are installed.
Modern formats on a platform that doesn’t speak them
Magento has no out-of-the-box support for WebP or AVIF — its image-processing pipeline produces and understands JPEG, PNG, and GIF, and that doesn’t change here. The platform keeps emitting exactly the formats it already knows how to make; the modern variants are layered alongside as sidecars by a separate step. You get AVIF/WebP delivery without waiting for — or hacking in — first-class format support in the application.
The same logic eases the production side. Whoever creates the source images — designers, content editors, a PIM feed, a photographer’s export — can keep using the tools they already know, exporting ordinary JPEG/PNG as they always have. They don’t need to learn WebP/AVIF export, install new encoders, or change their workflow; the conversion happens automatically downstream from whatever they upload.
Why the serving half stays out of PHP
A request for an image hits Apache/nginx, which answers it directly from disk in the common case. Magento’s get.php is only touched as a last-resort fallback when no cached file exists yet. So the hot path — repeat views of catalog and media images — is a static file transfer, while the expensive PHP image pipeline runs at most once per file.
flowchart TD
A["Browser requests /media/photo.jpg<br/>Accept: image/webp"] --> B["Web server<br/>nginx / Apache"]
B --> C{"Client accepts<br/>webp / avif?"}
C -- "Yes" --> D{"Sidecar<br/>photo.jpg.webp exists?"}
C -- "No" --> G{"Original<br/>photo.jpg exists?"}
D -- "Yes" --> E["Serve photo.jpg.webp"]
D -- "No" --> G
G -- "Yes" --> F["Serve original photo.jpg"]
G -- "No" --> H["Fallback to get.php<br/>Magento PHP process"]
E --> Z["Direct file transfer<br/>no PHP — fast path"]
F --> Z
classDef fast fill:#d6f5dd,stroke:#2e7d32,color:#1b3d23;
classDef php fill:#ffe0e0,stroke:#c62828,color:#5a1a1a;
class Z,E,F fast;
class H php;
nginx implements this with a map on $http_accept that sets a .webp/.avif suffix, plus try_files $uri$suffix $uri … /get.php. Apache does the same with .htaccess RewriteCond checks: if the client accepts the format and a sidecar exists on disk, rewrite to it; otherwise serve the original. Vary: Accept is set so CDNs and browsers cache the format variants separately. The conversion modules document these snippets but cannot install them — correct delivery depends on this web-server config existing.
The conversion framework
The base module ships no actual conversion of its own. It is a framework that does three things and delegates the rest:
- Finds candidates. A scanner walks the configured image folders (default
pub/media), driven by a cron job and animages:optimize:scanCLI command (with-lto list pending images into avar/…_image_optimizer.logfile). - Validates and routes. For each file it asks a set of validators whether the file needs conversion and which converter should handle it.
- Requests conversion. It dispatches a
brocode_convert_imageevent per file, carrying theimage_pathand theconverter_id. A default observer listens and runs the conversion.
The actual pixel work lives in small format modules that plug into this framework:
| Package | Role |
|---|---|
module-image-optimizer | Base framework: scan, validate, dispatch, converter registry |
module-image-optimizer-webp | WebP converter (registers into the base) |
module-image-optimizer-avif | AVIF converter; admin config for quality (0–100) and on/off; requires PHP ≥ 8.1 and ext-gd |
module-image-optimizer-queue | Runs conversion asynchronously via Magento’s MySQL queue |
module-image-optimizer-amqp | Routes that same queue over RabbitMQ |
Each format module is a thin unit: an Api config interface, a Model/Converter implementing the base converter interface, and etc wiring (di.xml registration plus admin system config). Installing one is all it takes to teach the shop a new format.
The AVIF module declares php: >=8.1 together with ext-gd, because GD’s imageavif() encoder only exists as of PHP 8.1 (and the GD build must be compiled with AVIF/libavif support). WebP is less demanding — imagewebp() has been available in GD since PHP 5.5. The minimum PHP version is a per-format property of the converter module, not of the base framework (whose own composer.json only requires magento/framework).
The wiring
The registry is built entirely with Magento DI array arguments:
- Scanner:
BroCode\ImageOptimizer\Model\ImagePathScannerServicetakes animagePathProvidersarray; the default entry isBroCode\ImageOptimizer\Model\Data\XmlConfigurableImagePathProvider, whosepathsarray defaults topub/media(the Magento base path is prepended automatically). - Path provider contract:
BroCode\ImageOptimizer\Api\Data\ImagePathProviderInterface. - Validation contract:
BroCode\ImageOptimizer\Api\Data\ImageConvertValidationInterface, with a file-check base inBroCode\ImageOptimizer\Model\Converter\AbstractImageConverter. - Registry:
BroCode\ImageOptimizer\Model\ImageConverterServicetakes two array arguments —imageValidatorandimageConverter. - Event + default observer:
brocode_convert_image(payloadimage_path,converter_id) handled byBroCode\ImageOptimizer\Observer\InstantConvertImageObserver.
One nuance worth noting: a format module registers the same class into both the imageValidator and the imageConverter arrays. A "converter" is therefore its own validator: by extending AbstractImageConverter (which implements the validation interface) one class answers both "does this file need converting, and by whom?" and "do the conversion." That keeps a format’s eligibility rules and its encoding logic in a single place.
Synchronous by default, async when it matters
Out of the box InstantConvertImageObserver catches the conversion event and converts synchronously — simple, but it slows the cron run when there are many images. Swapping to asynchronous processing is purely additive: install the queue module and scanning publishes conversion messages to Magento’s MySQL queue instead of converting inline; a BroCodeImageConversionConsumer consumes them in the background (and can be run as multiple parallel processes via env.php). Add the amqp module on top to move that queue onto RabbitMQ. The base module never learns that queues exist.
Separation of concerns
The codebase is split along independent axes of change:
- Decision vs. execution vs. delivery. Magento decides what to convert; the format modules execute the conversion; the web server handles delivery. Each layer is ignorant of the others’ internals.
- Mechanism vs. policy. The base supplies mechanism — scanning, validation, event dispatch, a converter registry. Each format module supplies policy: what "AVIF" means and at what quality.
- Discovery decoupled from config. Scan paths are injected, not hardcoded, so adding a directory is configuration rather than code.
- Processing strategy is swappable. Synchronous observer, MySQL queue, or RabbitMQ — chosen by which modules are installed, with no change to the core.
- Delivery is invisible to the application. Templates, modules, and emails are never involved in format selection, so the whole storefront (including third-party code) is upgraded without being touched or even inventoried.
Extensibility
Every common change maps to a single isolated unit of work, all through Magento DI and one event rather than inheritance or forks:
1. New scan locations — implement ImagePathProviderInterface and inject it into the scanner’s path-provider array.
2. Conversion eligibility — implement ImageConvertValidationInterface, or extend AbstractImageConverter for its built-in file checks.
3. New formats — register a converter object into ImageConverterService‘s imageConverter array in di.xml. Adding WebP or AVIF needs zero edits to the base module — textbook open/closed.
4. The event seam — anything can observe brocode_convert_image; this is exactly the hook the async modules use to redirect conversion into a queue.
5. Per-converter admin config — each format owns its own system configuration (quality, enable/disable), so formats evolve independently.
In short: new format → new converter module; new throughput model → queue/amqp; new directories → new path provider. Nothing requires touching the core.
Honest observations
- The performance footgun is opt-in to fix. Keeping the base synchronous keeps it dependency-light, but a naive install will bog down cron on a large catalog, and the operator has to know to add the queue module.
- Correct delivery lives outside the modules. The template-free serving depends on nginx/Apache config the modules document but cannot enforce. A correct conversion with a missing rewrite rule silently serves the original file.
Vary: Acceptfragments caches across formats — worth being aware of for CDN hit rates.
Who For
- Magento 2 merchants who want WebP or AVIF delivery without touching a single storefront template or auditing third-party extensions.
- Developers who want a clean, DI-extensible conversion pipeline — new formats, new scan paths, and new throughput models are each a one-module change.
- Operators on large catalogs who need a safe, incremental rollout path with async processing.
Who Skip
- Stores where a CDN or image service (Cloudinary, Imgix, Fastly IO) already handles format negotiation upstream.
- Projects that have not yet configured nginx or Apache content-negotiation rewrite rules — conversion runs, but browsers receive originals until the server config is in place.
Installation
composer require brocode/module-image-optimizer
composer require brocode/module-image-optimizer-webp
# AVIF requires PHP >= 8.1 and ext-gd compiled with libavif support:
composer require brocode/module-image-optimizer-avif
php bin/magento setup:upgrade
Add the nginx or Apache rewrite rules documented in the module README, then run the first scan manually:
php bin/magento images:optimize:scan
A cron job picks up new images on the configured schedule. Pass -l to list pending files into var/…_image_optimizer.log without converting.
Compatibility
| Component | Requirement |
|---|---|
| Magento | 2.4.x |
| PHP | ≥ 7.4 (base module) |
| PHP | ≥ 8.1 for the AVIF module |
| ext-gd | Required for WebP (PHP ≥ 5.5) and AVIF (compiled with libavif) |
FAQ
Does this touch any storefront templates?
No. Conversion and delivery are completely invisible above the web server. Any <img> tag pointing at a JPEG or PNG is transparently served as WebP or AVIF when the sidecar exists and the browser’s Accept header accepts it — including tags output by third-party modules and email templates.
What happens if a sidecar doesn’t exist yet?
The web server falls through to the original file. No errors, no broken images — graceful degradation is a first-class property of the sidecar design.
Will large catalogs bog down cron?
With synchronous conversion (the default), yes. Install module-image-optimizer-queue to push jobs to a background consumer. The base module does not change.
Can I add a custom directory to scan?
Yes. Implement ImagePathProviderInterface and inject it into the imagePathProviders array of ImagePathScannerService in your di.xml.
Can I add a completely new output format?
Yes. The base module ships no format of its own. Create a converter extending AbstractImageConverter, register it in both imageValidator and imageConverter arrays of ImageConverterService. Nothing in the core changes.
Does the base module enforce a minimum PHP version?
No — the base only requires magento/framework. The PHP floor is a per-format property: WebP needs PHP ≥ 5.5, AVIF needs PHP ≥ 8.1.