Converts PNG/JPG images to AVIF, WebP and JPG in multiple sizes.
Works with Laravel, Symfony, and vanilla PHP projects.
Modern web performance starts with images. They typically account for 50β80% of a page's total weight β and they are the first thing Lighthouse, PageSpeed Insights, and Core Web Vitals will flag.
AVIF and WebP deliver the same visual quality as JPEG at a fraction of the size:
| Format | Typical size vs JPG |
|---|---|
| JPG | baseline |
| WebP | ~30% smaller |
| AVIF | ~50% smaller |
The browser automatically picks the best format it supports via <picture> / srcset. JPG is kept as a universal fallback.
Serving a 1200px image on a 300px thumbnail wastes bandwidth and slows down the page. With this tool you define the exact widths you need per image category, and @2x / @3x variants are generated automatically for Retina and HiDPI screens β so every device downloads only what it needs.
Optimizing images directly improves the metrics that matter most:
- LCP (Largest Contentful Paint) β the main image loads faster
- Total Blocking Time β less network contention
- Serve images in next-gen formats β the most common PageSpeed recommendation, resolved
- Properly size images β resolved by generating the right widths
- Efficiently encode images β resolved by tunable per-format quality
Images are generated once at build time β no on-the-fly processing, no extra server load, no CDN dependency. The output is plain static files you deploy like any other asset.
Requires the PHP Imagick extension on your system.
# Ubuntu / Debian
sudo apt install -y php-imagick
# macOS
brew install imagemagick && pecl install imagickcomposer require --dev uxcode-fr/image-optimizerphp artisan vendor:publish --tag=image-optimizer-configcp vendor/uxcode-fr/image-optimizer/config/image-optimizer.php config/image-optimizer.phpreturn [
'source' => 'resources/images',
'destination' => 'public/img',
'quality' => [
'avif' => 60,
'webp' => 82,
'jpg' => 85,
],
'formats' => ['avif', 'webp', 'jpg'],
'densities' => [1, 2],
'folders' => [
'product' => [200, 280], // generates -200, -200@2x, -280, -280@2x variants
'author' => [48, 128],
'article' => null, // convert only, no resize
],
];{
"extra": {
"image-optimizer": {
"source": "resources/images",
"destination": "public/img",
"quality": { "avif": 60, "webp": 82, "jpg": 85 },
"formats": ["avif", "webp", "jpg"],
"densities": [1, 2],
"folders": {
"product": [200, 280],
"author": [48, 128]
}
}
}
}# Process all images
vendor/bin/image-optimizer
# Process only one folder
vendor/bin/image-optimizer --folder=product
# Force regeneration of existing images
vendor/bin/image-optimizer --force
# Delete generated images with no matching source
vendor/bin/image-optimizer --cleanπΌοΈ Optimizing product/hero.png (245 KB)...
β public/img/product/hero-200.avif (12 KB, -95% π)
β public/img/product/hero-200.webp (18 KB, -93% π)
β public/img/product/hero-200.jpg (22 KB, -91% π)
β public/img/product/hero-200@2x.avif (38 KB, -84% π)
β public/img/product/hero-200@2x.webp (52 KB, -79% π)
β public/img/product/hero-200@2x.jpg (61 KB, -75% π)
πΌοΈ Optimizing author/avatar.png (18 KB)...
β public/img/author/avatar-48.avif (skipped)
β public/img/author/avatar-48.webp (skipped)
β public/img/author/avatar-48.jpg (skipped)
6 image(s) generated, 3 skipped, 0 deleted.
<picture>
<source
type="image/avif"
srcset="
/img/product/hero-200.avif 1x,
/img/product/hero-200@2x.avif 2x
"
media="(max-width: 400px)"
>
<source
type="image/avif"
srcset="
/img/product/hero-280.avif 1x,
/img/product/hero-280@2x.avif 2x
"
>
<source
type="image/webp"
srcset="
/img/product/hero-200.webp 1x,
/img/product/hero-200@2x.webp 2x
"
media="(max-width: 400px)"
>
<source
type="image/webp"
srcset="
/img/product/hero-280.webp 1x,
/img/product/hero-280@2x.webp 2x
"
>
<img
src="/img/product/hero-280.jpg"
alt="Product"
width="280"
height="186"
loading="lazy"
>
</picture>The browser picks the first <source> it supports, from top to bottom β AVIF first, then WebP, then JPG as a universal fallback.
<img
src="/img/product/hero-280.jpg"
srcset="
/img/product/hero-200.webp 200w,
/img/product/hero-280.webp 280w
"
sizes="(max-width: 400px) 200px, 280px"
alt="Product"
loading="lazy"
><picture>
<source type="image/avif" srcset="/img/author/avatar-48.avif 1x, /img/author/avatar-48@2x.avif 2x">
<source type="image/webp" srcset="/img/author/avatar-48.webp 1x, /img/author/avatar-48@2x.webp 2x">
<img src="/img/author/avatar-48.jpg" alt="Author" width="48" height="48" loading="lazy">
</picture><picture>
<source
type="image/avif"
srcset="{{ asset('img/product/hero-200.avif') }} 1x, {{ asset('img/product/hero-200@2x.avif') }} 2x"
media="(max-width: 400px)"
>
<source
type="image/avif"
srcset="{{ asset('img/product/hero-280.avif') }} 1x, {{ asset('img/product/hero-280@2x.avif') }} 2x"
>
<img src="{{ asset('img/product/hero-280.jpg') }}" alt="Product" width="280" loading="lazy">
</picture>| Key | Default | Description |
|---|---|---|
source |
resources/images |
Source folder (PNG/JPG), relative to project root |
destination |
public/img |
Output folder, relative to project root |
quality |
['avif'=>60, 'webp'=>82, 'jpg'=>85] |
Compression quality per format (1β100) |
formats |
['avif', 'webp', 'jpg'] |
Output formats β any subset of avif, webp, jpg |
densities |
[1, 2] |
Pixel density multipliers (1 = base, 2 = @2xβ¦) |
folders |
[] |
Per-folder width list (null = convert only) |
Pass --clean to delete generated files that no longer have a matching source image (only for folders declared in folders).
MIT β Copyright 2026 uxcode.fr
