Реалізація автоматичної оптимізації зображень, що завантажуються (WebP/AVIF)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація автоматичної оптимізації зображень, що завантажуються (WebP/AVIF)
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація автоматичної оптимізації завантажених зображень (WebP/AVIF)

Неоптимізовані зображення — найчастіше головне джерело лишнього трафіку на сайті. PNG на 4 МБ замість WebP на 300 КБ — реальна історія з будь-якого проекту, де завантаження файлів додали без конвертації. Автоматизація на рівні бекенду знімає проблему незалежно від того, що саме завантажує користувач.

Формати та їх застосування

WebP — підтримується всіма браузерами з 2020 року. Lossy-компресія на 25–35% ефективніша за JPEG, lossless — на 25–34% краща за PNG. Хороший варіант за замовчуванням.

AVIF — заснований на AV1, ще ефективніший за WebP приблизно на 20–30%, особливо на фотографіях. Мінуси: кодування повільніше (в 5–10 разів на libavif), підтримка в Safari з'явилася з версії 16.4. На 2025 рік охоплення браузерів — близько 93%.

Практичний підхід: генерувати обидва формати, віддавати через <picture> з type="image/avif" в першому <source>.

Стек

На сервері обробку краще підняти через sharp (Node.js) або Intervention Image з Imagick (PHP). Для чисто серверної оптимізації без прив'язки до фреймворку — squoosh-cli або cjpeg/cwebp від Google як системні утиліти.

Для PHP-проектів intervention/image працює з Imagick, який підтримує WebP, AVIF (з ImageMagick 7.0.25+).

Перевірка версії на сервері:

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

Якщо версія Imagick нижче 7 — AVIF недоступний. Тоді для AVIF використовуємо libavif через avifenc:

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

Сервіс оптимізації (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 шкала інша, 55 ≈ JPEG 85
    private const MAX_WIDTH     = 2560;

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

        // Не збільшуємо маленькі зображення
        if ($img->width() > self::MAX_WIDTH) {
            $img->resize(self::MAX_WIDTH, null, function ($c) {
                $c->aspectRatio();
                $c->upsize();
            });
        }

        // Видаляємо EXIF (приватні дані + вага)
        $img->orientate(); // застосовуємо EXIF-орієнтацію перед скиданням

        $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 через avifenc якщо доступний, інакше через 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;
    }
}

Фонова обробка через Job

Конвертація AVIF — повільна операція (до 2–5 секунд на фото). Тримати HTTP-запит відкритим все це час не потрібно. Паттерн: спочатку зберегти оригінал, одразу відповісти клієнту, у фоні запустити оптимізацію.

// 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);

        // Створюємо UploadedFile з існуючого файлу
        $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(),
        ]);
    }
}

Диспатч після завантаження оригіналу:

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

Віддача правильного формату

На рівні Blade/фронтенду використовуємо <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-варіант для автоматичної віддачі WebP без зміни коду (якщо файл існує):

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

Працює тільки якщо WebP-файл лежить поруч з оригіналом з додаванням .webp до імені.

Сроки

Установка та настройка стека, сервіс оптимізації — 5–7 годин. Фоновий Job, інтеграція з моделлю, Blade-компонент <picture> — ще 4–5 годин. Настройка Nginx-варіанту без зміни шаблонів — 1–2 години окремо.