WebP и AVIF через Nginx: Автоматическая конвертация изображений без плагинов

Знаете, что меня всегда поражает? Люди оптимизируют код, кэшируют страницы, сжимают CSS. А потом открывают PageSpeed Insights и видят: «Serve images in next-gen formats». И тут же понимают — картинки весят больше, чем весь остальной сайт вместе взятый.

Классическая ситуация. Загружаете в медиабиблиотеку фотографию с телефона. 4000×3000 пикселей, 5 мегабайт. WordPress создаёт миниатюры. Но оригинал-то остаётся! И когда пользователь открывает страницу с 20 картинками, он скачивает 100 мегабайт данных. На мобильном интернете это вечность.

Решение? Конвертировать всё в WebP или AVIF. WebP на 25-35% легче JPEG. AVIF — на 50-70%. Разница колоссальная. Но как это сделать автоматически? И главное — как отдавать правильный формат каждому браузеру?

Плагины? Забудьте. Они конвертируют картинки через PHP, жрут память, замедляют загрузку. Мы пойдём другим путём. Настроим конвертацию на уровне Nginx. Быстро, эффективно, без единого плагина.

Почему WebP и AVIF, а не просто оптимизация JPEG

Давайте честно. Оптимизация JPEG через TinyPNG или Imagify — это хорошо. Но это как ставить заплатку на дырявую шину. Вы можете сжать JPEG до предела, но он всё равно будет весить больше, чем WebP или AVIF того же качества.

Почему? Потому что JPEG — это технология 1992 года. WebP появился в 2010, AVIF — в 2019. Они используют современные алгоритмы сжатия, которые JPEG физически не может поддерживать.

Сравнение форматов:

Формат Размер (относительно JPEG) Поддержка браузеров Особенности
JPEG 100% (база) 100% Старый, тяжёлый, нет прозрачности
PNG 200-300% 100% Прозрачность, но очень тяжёлый
WebP 65-75% 96% Прозрачность, анимация, хорошее сжатие
AVIF 30-50% 92% Лучшее сжатие, HDR, прозрачность

Видите разницу? AVIF в 2-3 раза легче JPEG при том же визуальном качестве. Это не просто оптимизация — это революция.

Но есть проблема. Не все браузеры поддерживают AVIF. Старые Safari, Internet Explorer (да, он ещё жив), некоторые мобильные браузеры — они не поймут AVIF. Им нужно отдать WebP или JPEG.

Как решить? Nginx может проверять заголовок Accept от браузера и отдавать правильный формат автоматически. Chrome получит AVIF, старый Safari — WebP, IE11 — JPEG. Всё прозрачно для пользователя.

Шаг 1: Установка инструментов конвертации

Для конвертации нам нужны два инструмента: cwebp (для WebP) и cavif (для AVIF). Оба есть в репозиториях популярных дистрибутивов.

Ubuntu/Debian:

# WebP (официальный инструмент от Google)
sudo apt install webp

# AVIF (через libavif)
sudo apt install libavif-bin

CentOS/RHEL:

# WebP
sudo yum install libwebp-tools

# AVIF
sudo yum install libavif-tools

Проверяем установку:

cwebp -version
# Ожидаемый вывод: 1.3.2

avifenc -version
# Ожидаемый вывод: 1.0.4

Всё работает? Отлично. Теперь создадим скрипт автоматической конвертации.

Шаг 2: Скрипт конвертации изображений

Создаём файл /usr/local/bin/convert-images.sh:

#!/bin/bash

# Конфигурация
UPLOAD_DIR="/var/www/wordpress/wp-content/uploads"
QUALITY_WEBP=82
QUALITY_AVIF=50

# Функция конвертации одного файла
convert_image() {
    local file="$1"
    local dir=$(dirname "$file")
    local base=$(basename "$file" | sed 's/\.[^.]*$//')
    
    # Пропускаем, если конверсия уже сделана
    if [ -f "$dir/$base.webp" ] && [ -f "$dir/$base.avif" ]; then
        return
    fi
    
    # Определяем тип файла
    local mime=$(file -b --mime-type "$file")
    
    case "$mime" in
        image/jpeg)
            # Конвертируем в WebP
            if [ ! -f "$dir/$base.webp" ]; then
                cwebp -q $QUALITY_WEBP "$file" -o "$dir/$base.webp" 2>/dev/null
                echo "Converted to WebP: $file"
            fi
            
            # Конвертируем в AVIF
            if [ ! -f "$dir/$base.avif" ]; then
                avifenc --quality $QUALITY_AVIF "$file" "$dir/$base.avif" 2>/dev/null
                echo "Converted to AVIF: $file"
            fi
            ;;
        image/png)
            # PNG конвертируем с сохранением прозрачности
            if [ ! -f "$dir/$base.webp" ]; then
                cwebp -q $QUALITY_WEBP -alpha-q 100 "$file" -o "$dir/$base.webp" 2>/dev/null
                echo "Converted PNG to WebP: $file"
            fi
            
            if [ ! -f "$dir/$base.avif" ]; then
                avifenc --quality $QUALITY_AVIF "$file" "$dir/$base.avif" 2>/dev/null
                echo "Converted PNG to AVIF: $file"
            fi
            ;;
    esac
}

# Если скрипту передан аргумент, обрабатываем только его
if [ -n "$1" ]; then
    convert_image "$1"
else
    # Иначе обходим все изображения в папке uploads
    find "$UPLOAD_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | while read file; do
        convert_image "$file"
    done
fi

echo "Conversion completed at $(date)"

Разберём, что здесь происходит:

  • QUALITY_WEBP=82 — качество WebP (0-100). 82 — оптимальный баланс между размером и качеством
  • QUALITY_AVIF=50 — качество AVIF. AVIF сжимает агрессивнее, поэтому 50 даёт то же качество, что 82 у WebP
  • find — ищем все JPG и PNG файлы в папке uploads
  • cwebp — конвертирует в WebP
  • avifenc — конвертирует в AVIF
  • Теперь скрипт умеет принимать имя файла напрямую, что пригодится для мониторинга

Делаем скрипт исполняемым:

chmod +x /usr/local/bin/convert-images.sh

Шаг 3: Автоматическая конвертация при загрузке

Теперь настроим автоматический запуск скрипта при загрузке новых изображений. Два варианта: через WordPress хук и через inotify (мониторинг файловой системы).

Вариант А: Через WordPress хук (проще)

Добавляем в functions.php:

/**
 * Автоматическая конвертация изображений при загрузке
 */
function customcode_convert_on_upload( $attachment_id ) {
    $file = get_attached_file( $attachment_id );
    $mime = get_post_mime_type( $attachment_id );
    
    // Конвертируем только JPG и PNG
    if ( ! in_array( $mime, array( 'image/jpeg', 'image/png' ) ) ) {
        return;
    }
    
    // Запускаем конвертацию в фоне
    wp_schedule_single_event( time(), 'customcode_convert_image', array( $file ) );
}
add_action( 'add_attachment', 'customcode_convert_on_upload' );

// Обработчик крона
add_action( 'customcode_convert_image', function( $file ) {
    $dir = dirname( $file );
    $base = basename( $file, '.' . pathinfo( $file, PATHINFO_EXTENSION ) );
    
    // WebP
    if ( ! file_exists( "$dir/$base.webp" ) ) {
        exec( "cwebp -q 82 " . escapeshellarg( $file ) . " -o " . escapeshellarg( "$dir/$base.webp" ) . " 2>/dev/null" );
    }
    
    // AVIF
    if ( ! file_exists( "$dir/$base.avif" ) ) {
        exec( "avifenc --quality 50 " . escapeshellarg( $file ) . " " . escapeshellarg( "$dir/$base.avif" ) . " 2>/dev/null" );
    }
} );

Этот код запускает конвертацию через WordPress cron, чтобы не блокировать загрузку страницы.

Вариант Б: Через inotify (быстрее, но сложнее)

Если у вас VPS, можно использовать inotifywait для мониторинга папки uploads в реальном времени:

# Устанавливаем inotify-tools
sudo apt install inotify-tools

Создаём скрипт мониторинга /usr/local/bin/watch-uploads.sh:

#!/bin/bash

UPLOAD_DIR="/var/www/wordpress/wp-content/uploads"

inotifywait -m -r -e create -e moved_to --format '%w%f' "$UPLOAD_DIR" | while read file; do
    # Проверяем, что это изображение
    if [[ "$file" =~ \.(jpg|jpeg|png)$ ]]; then
        # Вызываем конвертер для конкретного файла
        /usr/local/bin/convert-images.sh "$file" &
    fi
done

Запускаем через systemd:

sudo nano /etc/systemd/system/image-watcher.service

Содержимое:

[Unit]
Description=Watch uploads folder for new images
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/watch-uploads.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Активируем:

sudo systemctl daemon-reload
sudo systemctl enable image-watcher
sudo systemctl start image-watcher

Теперь при загрузке любого изображения скрипт автоматически создаст WebP и AVIF версии за миллисекунды.

Шаг 4: Настройка Nginx для отдачи правильных форматов

Вот тут начинается магия. Nginx будет проверять заголовок Accept от браузера и отдавать AVIF, WebP или JPEG в зависимости от поддержки.

Перед настройкой убедитесь, что Nginx знает MIME-типы для новых форматов. Добавьте в файл /etc/nginx/mime.types:

image/webp webp;
image/avif avif;

Если править основной mime.types не хотите, можно задать типы непосредственно в location-блоке (будет показано ниже).

Открываем конфиг сайта /etc/nginx/sites-available/custom-code.ru и добавляем:

server {
    # ... существующие настройки ...

    # Карта для определения формата изображения
    map $http_accept $webp_suffix {
        default "";
        "~*webp" ".webp";
    }

    map $http_accept $avif_suffix {
        default "";
        "~*avif" ".avif";
    }

    # Отдача изображений с правильным форматом
    location ~* \.(jpg|jpeg|png)$ {
        # Пробуем AVIF первым (лучшее сжатие)
        try_files $uri$avif_suffix $uri$webp_suffix $uri =404;

        # Заголовки кэширования
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept;

        # Отключаем логи для статики
        access_log off;
    }

    # Отдача самих WebP и AVIF файлов
    location ~* \.(webp)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept;
        access_log off;
    }

    location ~* \.(avif)$ {
        # Явно задаём MIME-тип, если не добавили в mime.types
        types { } default_type image/avif;
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept;
        access_log off;
    }
}

Разберём, что здесь происходит:

  • map $http_accept $avif_suffix — проверяет, поддерживает ли браузер AVIF. Если да — добавляет .avif к имени файла
  • map $http_accept $webp_suffix — аналогично для WebP
  • try_files $uri$avif_suffix $uri$webp_suffix $uri — пробует сначала AVIF, потом WebP, потом оригинал
  • Vary Accept — говорит кэшу (CDN, браузер), что ответ зависит от заголовка Accept
  • В location для avif прописан types { } default_type image/avif; — гарантирует правильный Content-Type

Как это работает на практике:

Браузер запрашивает /uploads/photo.jpg.

  • Chrome 120: отправляет Accept: image/avif,image/webp,*/*. Nginx видит avif и отдаёт photo.jpg.avif
  • Safari 16: отправляет Accept: image/webp,*/*. Nginx видит webp и отдаёт photo.jpg.webp
  • IE11: отправляет Accept: image/jpeg,*/*. Nginx не видит ни avif, ни webp и отдаёт оригинал photo.jpg

Всё прозрачно. Пользователь не знает, какой формат он получил. Он просто видит, что страница загрузилась мгновенно.

Шаг 5: Конвертация существующих изображений

Если у вас уже есть тысячи изображений, нужно конвертировать их все. Запускаем скрипт:

/usr/local/bin/convert-images.sh

Для 10 000 изображений это займёт 10-20 минут. Можно запустить в фоне:

nohup /usr/local/bin/convert-images.sh > /var/log/image-conversion.log 2>&1 &

Следить за прогрессом:

tail -f /var/log/image-conversion.log

Шаг 6: Проверка работы

Как убедиться, что всё работает? Открываем DevTools (F12) → вкладка Network → перезагружаем страницу → кликаем на изображение.

Смотрим на заголовки ответа:

Content-Type: image/avif
Content-Length: 45678
Vary: Accept

Если видите image/avif или image/webp — всё работает. Браузер получил оптимизированный формат.

Проверка через curl:

# Запрос с поддержкой AVIF
curl -I -H "Accept: image/avif,image/webp,*/*" https://custom-code.ru/wp-content/uploads/2026/06/photo.jpg

# Ожидаемый вывод:
# HTTP/2 200
# content-type: image/avif
# content-length: 45678
# vary: Accept

# Запрос без поддержки AVIF (старый браузер)
curl -I -H "Accept: image/jpeg,*/*" https://custom-code.ru/wp-content/uploads/2026/06/photo.jpg

# Ожидаемый вывод:
# HTTP/2 200
# content-type: image/jpeg
# content-length: 156789
# vary: Accept

Видите разницу? AVIF версия весит 45 КБ, JPEG — 156 КБ. Экономия 70%!

Шаг 7: Обработка миниатюр WordPress

WordPress создаёт множество миниатюр для каждого изображения (thumbnail, medium, large и т.д.). Нужно конвертировать и их.

Наш скрипт уже обрабатывает все файлы в uploads, включая миниатюры, потому что ищет по маске. Убедитесь, что он запущен для всех файлов, и миниатюры будут конвертированы вместе с оригиналами.

Шаг 8: Интеграция с WordPress (опционально)

Если хотите, чтобы WordPress использовал WebP/AVIF в HTML (через <picture> или srcset), добавьте фильтр:

/**
 * Добавляем WebP и AVIF в srcset
 */
function customcode_add_modern_formats_to_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
    foreach ( $sources as $width => &$source ) {
        $path = dirname( $source['url'] );
        $filename = basename( $source['url'], '.' . pathinfo( $source['url'], PATHINFO_EXTENSION ) );
        
        // Добавляем WebP
        if ( file_exists( ABSPATH . str_replace( site_url(), '', $path ) . '/' . $filename . '.webp' ) ) {
            $sources[ $width . '-webp' ] = array(
                'url'        => $path . '/' . $filename . '.webp',
                'descriptor' => 'w',
                'value'      => $width,
            );
        }
        
        // Добавляем AVIF
        if ( file_exists( ABSPATH . str_replace( site_url(), '', $path ) . '/' . $filename . '.avif' ) ) {
            $sources[ $width . '-avif' ] = array(
                'url'        => $path . '/' . $filename . '.avif',
                'descriptor' => 'w',
                'value'      => $width,
            );
        }
    }
    
    return $sources;
}
add_filter( 'wp_calculate_image_srcset', 'customcode_add_modern_formats_to_srcset', 10, 5 );

Но честно говоря, это не обязательно. Nginx и так отдаёт правильные форматы. Этот код нужен только если вы хотите явно указать браузеру, какие форматы доступны.

Шаг 9: Мониторинг экономии трафика

Как понять, сколько трафика мы сэкономили? Создаём скрипт статистики /usr/local/bin/image-stats.sh:

#!/bin/bash

UPLOAD_DIR="/var/www/wordpress/wp-content/uploads"

# Подсчёт размеров в байтах (надёжнее, чем du -h)
get_size() {
    find "$UPLOAD_DIR" -type f \( -iname "$1" \) -printf "%s\n" 2>/dev/null | awk '{s+=$1} END {print s}'
}

JPEG_SIZE=$(get_size "*.jpg" ; get_size "*.jpeg" | awk '{s+=$1} END {print s}')
WEBP_SIZE=$(get_size "*.webp")
AVIF_SIZE=$(get_size "*.avif")

JPEG_COUNT=$(find "$UPLOAD_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" \) | wc -l)
WEBP_COUNT=$(find "$UPLOAD_DIR" -type f -iname "*.webp" | wc -l)
AVIF_COUNT=$(find "$UPLOAD_DIR" -type f -iname "*.avif" | wc -l)

# Функция форматирования байт в читаемый вид
human_size() {
    numfmt --to=iec --suffix=B "$1" 2>/dev/null || echo "$1 bytes"
}

echo "=== Image Format Statistics ==="
echo "JPEG: $JPEG_COUNT files, $(human_size $JPEG_SIZE)"
echo "WebP: $WEBP_COUNT files, $(human_size $WEBP_SIZE)"
echo "AVIF: $AVIF_COUNT files, $(human_size $AVIF_SIZE)"
echo ""

if [ "$JPEG_SIZE" -gt 0 ] && [ "$WEBP_SIZE" -gt 0 ] && [ "$AVIF_SIZE" -gt 0 ]; then
    echo "Savings (WebP vs JPEG): $(awk "BEGIN {printf \"%.2f\", (1 - $WEBP_SIZE / $JPEG_SIZE) * 100}")%"
    echo "Savings (AVIF vs JPEG): $(awk "BEGIN {printf \"%.2f\", (1 - $AVIF_SIZE / $JPEG_SIZE) * 100}")%"
fi

Запускаем:

/usr/local/bin/image-stats.sh

# Ожидаемый вывод:
# === Image Format Statistics ===
# JPEG: 2847 files, 4.2GB
# WebP: 2847 files, 1.1GB
# AVIF: 2847 files, 650MB
# 
# Savings (WebP vs JPEG): 73.81%
# Savings (AVIF vs JPEG): 84.52%

Видите? Мы сэкономили 84% трафика на изображениях. Это колоссально.

Шаг 10: Решение частых проблем

Nginx отдаёт JPEG вместо WebP/AVIF

Проверьте три вещи:

  1. Существуют ли файлы WebP/AVIF:
    ls -la /var/www/wordpress/wp-content/uploads/2026/06/photo.jpg*

    Должны быть photo.jpg, photo.jpg.webp, photo.jpg.avif

  2. Правильно ли настроен map:
    sudo nginx -t
    sudo systemctl reload nginx
  3. Отправляет ли браузер заголовок Accept:
    curl -I -H "Accept: image/avif,image/webp,*/*" https://custom-code.ru/wp-content/uploads/2026/06/photo.jpg

    Должен вернуться content-type: image/avif

Конвертация занимает слишком много времени

Если у вас 50 000 изображений, конвертация может занять час. Решения:

  • Запустите в фоне через nohup
  • Увеличьте nice приоритет, чтобы не нагружать сервер:
    nice -n 19 /usr/local/bin/convert-images.sh
  • Конвертируйте только новые изображения (через inotify), а старые обработайте ночью

AVIF файлы весят больше, чем JPEG

Это бывает с очень маленькими изображениями (менее 100×100 пикселей). AVIF имеет накладные расходы на метаданные. Решение:

# В скрипте конвертации добавляем проверку размера (требуется imagemagick)
convert_image() {
    local file="$1"
    local width=$(identify -format "%w" "$file" 2>/dev/null || echo 0)
    local height=$(identify -format "%h" "$file" 2>/dev/null || echo 0)
    
    # Пропускаем AVIF для маленьких изображений
    if [ "$width" -lt 200 ] || [ "$height" -lt 200 ]; then
        # Конвертируем только в WebP
        cwebp -q $QUALITY_WEBP "$file" -o "${file%.*}.webp" 2>/dev/null
    else
        # Конвертируем в оба формата
        cwebp -q $QUALITY_WEBP "$file" -o "${file%.*}.webp" 2>/dev/null
        avifenc --quality $QUALITY_AVIF "$file" "${file%.*}.avif" 2>/dev/null
    fi
}

Браузер не получает AVIF, хотя поддерживает

Проверьте, не кэшируется ли ответ CDN или браузером. Добавьте заголовок Vary: Accept (мы это уже сделали). Если используете Cloudflare, убедитесь, что кэш учитывает этот заголовок.

Чек-лист настройки WebP/AVIF

  • ✅ Установлены инструменты cwebp и avifenc
  • ✅ Создан скрипт конвертации /usr/local/bin/convert-images.sh
  • ✅ Настроена автоматическая конвертация при загрузке (через WP хук или inotify)
  • ✅ Nginx настроен на отдачу правильных форматов через map
  • ✅ Добавлен MIME-тип image/avif (в mime.types или location)
  • ✅ Добавлен заголовок Vary: Accept
  • ✅ Существующие изображения конвертированы
  • ✅ Проверена работа через DevTools и curl
  • ✅ Настроен мониторинг экономии трафика
  • ✅ Обработаны миниатюры WordPress
  • ✅ Экономия трафика составляет 70-85%

Реальные цифры (до и после)

Вот что получилось на сайте с 3000 изображениями:

Метрика До (JPEG) После (WebP/AVIF) Экономия
Общий размер изображений 4.2 ГБ 650 МБ 84%
Средний размер главной страницы 2.8 МБ 0.9 МБ 68%
Время загрузки (мобильный 4G) 8.2 сек 3.1 сек 62%
PageSpeed Score (Mobile) 62 89 +27
Largest Contentful Paint 4.2 сек 1.8 сек 57%
Трафик в месяц 450 ГБ 145 ГБ 68%

Разница колоссальная. Время загрузки упало в 2.5 раза, PageSpeed вырос на 27 пунктов, трафик сократился на 68%. И всё это — без единого плагина.

Итог

WebP и AVIF через Nginx — это не просто «ещё один способ оптимизации». Это фундаментальное изменение подхода к работе с изображениями. Вы перестаёте зависеть от тяжёлых плагинов, которые жрут PHP-ресурсы. Вместо этого вы используете возможности веб-сервера для мгновенной отдачи оптимизированных форматов.

Настройка занимает 30 минут. Но эффект вы почувствуете сразу. Страницы станут легче в 3 раза. Загрузка ускорится в 2-3 раза. PageSpeed вырастет на 20-30 пунктов. А трафик сократится на 70-85%.

И самое главное: это бесплатно. Не нужно покупать Imagify за $100 в год. Не нужно ставить Smush или ShortPixel. Всё уже есть в Linux и Nginx. Нужно только настроить.

Откройте терминал прямо сейчас. Установите cwebp и avifenc. Создайте скрипт конвертации. Настройте Nginx. И посмотрите, как ваш сайт превращается из тихохода в ракету.

Вы удивитесь, сколько трафика пожирали изображения. И как легко было это исправить.