Image Optimization: WebP, AVIF, lazy loading
Images — 50–70% of average webpage weight. WebP conversion saves 25–35% vs JPEG; AVIF — another 20% on top. Lazy loading saves bandwidth and accelerates first screen load.
Formats: JPEG → WebP → AVIF
| Format | Browser support | Savings vs JPEG |
|---|---|---|
| JPEG | 100% | — |
| WebP | 96% | 25–35% |
| AVIF | 88% | 40–60% |
| WebP (lossless) | 96% | 20–30% vs PNG |
<picture> element for progressive fallback:
<picture>
<source type="image/avif"
srcset="/images/product-400.avif 400w,
/images/product-800.avif 800w,
/images/product-1200.avif 1200w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px">
<source type="image/webp"
srcset="/images/product-400.webp 400w,
/images/product-800.webp 800w,
/images/product-1200.webp 1200w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px">
<img src="/images/product-800.jpg"
width="800" height="800"
alt="Product name"
loading="lazy"
decoding="async">
</picture>
Generating converted formats in Laravel
// Using spatie/laravel-medialibrary with conversions
class Product extends Model
{
use InteractsWithMedia;
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(400)->height(400)
->format('webp')
->quality(80)
->performOnCollections('images');
$this->addMediaConversion('medium')
->width(800)->height(800)
->format('webp')
->quality(82)
->performOnCollections('images');
$this->addMediaConversion('avif-medium')
->width(800)->height(800)
->format('avif')
->quality(65)
->performOnCollections('images');
}
}
// Blade component for adaptive image
// resources/views/components/image.blade.php
@props(['media', 'alt', 'sizes' => '100vw', 'loading' => 'lazy'])
<picture>
@if ($media->hasGeneratedConversion('avif-medium'))
<source type="image/avif"
srcset="{{ $media->getUrl('avif-medium') }}"
sizes="{{ $sizes }}">
@endif
<source type="image/webp"
srcset="{{ $media->getUrl('medium') }}"
sizes="{{ $sizes }}">
<img src="{{ $media->getUrl() }}"
width="{{ $media->getCustomProperty('width') }}"
height="{{ $media->getCustomProperty('height') }}"
alt="{{ $alt }}"
loading="{{ $loading }}"
decoding="async">
</picture>
Nginx: automatic WebP serving
If no on-the-fly generation — convert ahead and serve via Nginx:
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
location ~* \.(jpg|jpeg|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
expires 30d;
}
Lazy loading
<!-- Native lazy loading -->
<img src="photo.webp" loading="lazy" alt="...">
<!-- LCP image — NEVER lazy -->
<img src="hero.webp" loading="eager" fetchpriority="high" alt="...">
Native lazy loading supported in all modern browsers. Images load when entering viewport with small buffer (usually 1200px before appearance).
Intersection Observer for custom lazy loading
// For background-image and non-standard cases
function lazyLoadBackgrounds() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target as HTMLElement;
el.style.backgroundImage = `url(${el.dataset.bg})`;
el.removeAttribute('data-bg');
observer.unobserve(el);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('[data-bg]').forEach(el => observer.observe(el));
}
Quality optimization
Typical quality recommendations:
| Image type | WebP quality | AVIF quality |
|---|---|---|
| Product photo | 82–85 | 65–70 |
| Hero image | 85–88 | 70–75 |
| Icons, logos | Use SVG | — |
| Screenshots, UI | 90 | 75 |
Blur placeholder (LQIP)
// Generate tiny placeholder on upload
use Intervention\Image\ImageManager;
$manager = ImageManager::gd();
$image = $manager->read($file->path());
$lqip = $image->scale(width: 20)
->toJpeg(quality: 30)
->toDataUri(); // base64 data URI
<img src="{{ $lqip }}"
data-src="/images/product-full.webp"
style="filter: blur(10px); transition: filter 0.3s"
onload="this.style.filter='none'; this.src=this.dataset.src"
alt="...">
Optimization time: 1–2 days for conversion and media library setup.







