Automatic image optimization on upload to WebP and AVIF

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Automatic image optimization on upload to WebP and AVIF
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing Automatic Image Optimization (WebP/AVIF)

Unoptimized images — usually the main source of unnecessary traffic on sites. 4 MB PNG instead of 300 KB WebP — real story from any project where file upload was added without conversion. Backend automation solves problem regardless of what user uploads.

Formats and Their Application

WebP — supported by all browsers since 2020. Lossy compression 25–35% more efficient than JPEG, lossless 25–34% better than PNG. Good default option.

AVIF — based on AV1, even more efficient than WebP by 20–30%, especially on photos. Downsides: encoding slower (5–10× on libavif), Safari support appeared in v16.4. As of 2025, browser coverage ~93%.

Practical approach: generate both formats, serve via <picture> with type="image/avif" in first <source>.

Stack

For server processing, better sharp (Node.js) or Intervention Image with Imagick (PHP). For pure server optimization without framework — squoosh-cli or cjpeg/cwebp from Google as system utilities.

For PHP projects intervention/image works with Imagick, which supports WebP, AVIF (from ImageMagick 7.0.25+).

Check version on server:

convert --version
# ImageMagick 7.1.x ...
php -r "echo Imagick::getVersion()['versionString'];"

If Imagick version below 7 — AVIF unavailable. Then for AVIF use libavif via avifenc:

apt install libavif-bin
avifenc --speed 6 --quality 50 input.png output.avif

Optimization Service (PHP/Laravel)

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class ImageOptimizationService
{
    private const WEBP_QUALITY  = 82;
    private const AVIF_QUALITY  = 55; // AVIF scale different, 55 ≈ JPEG 85
    private const MAX_WIDTH     = 2560;

    public function optimize(UploadedFile $file, string $storagePath): array
    {
        $img = Image::make($file);

        // Don't enlarge small images
        if ($img->width() > self::MAX_WIDTH) {
            $img->resize(self::MAX_WIDTH, null, function ($c) {
                $c->aspectRatio();
                $c->upsize();
            });
        }

        // Remove EXIF (private data + weight)
        $img->orientate(); // apply EXIF orientation before stripping

        $hash     = hash('xxh3', file_get_contents($file->getRealPath()));
        $dir      = rtrim($storagePath, '/');
        $result   = [];

        // WebP
        $webpPath = "{$dir}/{$hash}.webp";
        Storage::disk('public')->put(
            $webpPath,
            (string) $img->encode('webp', self::WEBP_QUALITY)
        );
        $result['webp'] = $webpPath;

        // AVIF via avifenc if available, otherwise via Imagick
        $avifPath = "{$dir}/{$hash}.avif";
        if ($this->avifEncAvailable()) {
            $result['avif'] = $this->encodeAvifViaCli(
                $img, $avifPath, self::AVIF_QUALITY
            );
        } elseif ($this->imagickSupportsAvif()) {
            $imagick = new \Imagick();
            $imagick->readImageBlob((string) $img->encode('png'));
            $imagick->setImageFormat('avif');
            $imagick->setImageCompressionQuality(self::AVIF_QUALITY);
            Storage::disk('public')->put($avifPath, $imagick->getImageBlob());
            $result['avif'] = $avifPath;
        }

        return $result;
    }

    private function avifEncAvailable(): bool
    {
        return !empty(shell_exec('which avifenc 2>/dev/null'));
    }

    private function imagickSupportsAvif(): bool
    {
        return in_array('AVIF', (new \Imagick())->queryFormats('AVIF'), true);
    }

    private function encodeAvifViaCli(\Intervention\Image\Image $img, string $storagePath, int $quality): string
    {
        $tmpIn  = tempnam(sys_get_temp_dir(), 'avif_in_')  . '.png';
        $tmpOut = tempnam(sys_get_temp_dir(), 'avif_out_') . '.avif';

        file_put_contents($tmpIn, (string) $img->encode('png'));
        exec("avifenc --speed 6 --quality {$quality} {$tmpIn} {$tmpOut} 2>&1");

        Storage::disk('public')->put($storagePath, file_get_contents($tmpOut));
        unlink($tmpIn);
        unlink($tmpOut);

        return $storagePath;
    }
}

Background Processing via Job

AVIF conversion — slow operation (2–5 seconds per photo). Keep HTTP request open all that time — not needed. Pattern: save original first, respond immediately, run optimization in background.

// app/Jobs/OptimizeImageJob.php
class OptimizeImageJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 3;
    public int $timeout = 120;

    public function __construct(
        private int    $mediaId,
        private string $originalPath
    ) {}

    public function handle(ImageOptimizationService $service): void
    {
        $media = Media::findOrFail($this->mediaId);

        // Create UploadedFile from existing file
        $fullPath = Storage::disk('public')->path($this->originalPath);
        $file = new \Illuminate\Http\UploadedFile($fullPath, basename($fullPath));

        $variants = $service->optimize($file, dirname($this->originalPath));

        $media->update([
            'variants'     => array_merge($media->variants ?? [], $variants),
            'optimized_at' => now(),
        ]);
    }
}

Dispatch after original upload:

$media = Media::create(['path' => $originalPath, ...]);
OptimizeImageJob::dispatch($media->id, $originalPath)->onQueue('media');

Serving Correct Format

At Blade/frontend level use <picture>:

<picture>
  @if($media->variantUrl('avif'))
    <source srcset="{{ $media->variantUrl('avif') }}" type="image/avif">
  @endif
  <source srcset="{{ $media->variantUrl('webp') }}" type="image/webp">
  <img src="{{ $media->url }}" alt="{{ $alt }}" loading="lazy" decoding="async">
</picture>

Nginx variant for automatic WebP serving without code changes (if file exists):

location ~* \.(jpe?g|png)$ {
    add_header Vary Accept;
    try_files
        $uri.webp
        $uri
        =404;
}

Works only if WebP file lies next to original with .webp added to name.

Timeline

Setup and config stack, optimization service — 5–7 hours. Background Job, model integration, <picture> component — another 4–5 hours. Nginx variant without template changes — 1–2 hours separately.