Реализация автоматической оптимизации загружаемых изображений (WebP/AVIF)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация автоматической оптимизации загружаемых изображений (WebP/AVIF)
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация автоматической оптимизации загружаемых изображений (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 часа отдельно.