Изображения составляют 50-80% веса типичной WordPress-страницы. Даже при включённом lazy loading и правильных размерах, JPEG и PNG остаются неоправданно тяжёлыми. Современные форматы WebP и AVIF обеспечивают то же визуальное качество при размере на 30-70% меньше, что напрямую влияет на метрики Core Web Vitals (LCP, CLS).
Популярные плагины вроде ShortPixel или Imagify конвертируют изображения при загрузке в WordPress, что создаёт три проблемы: нагрузка на PHP при каждой загрузке медиафайла, зависимость от сторонних API (часто платных), и невозможность конвертировать уже загруженные тысячи изображений без долгих процессов. В этом руководстве настраивается автоматическая конвертация на уровне Nginx — изображения конвертируются один раз при первом запросе и кэшируются, а WordPress вообще не знает об этом процессе.
Сравнение форматов изображений
Прежде чем настраивать конвертацию, стоит понять разницу между форматами и их поддержку браузерами.
| Формат | Сжатие vs JPEG | Поддержка браузеров | Особенности |
|---|---|---|---|
| JPEG | Базовый | 100% | Устаревший, но универсальный |
| WebP | На 25-35% меньше | 97% (все современные) | Поддержка прозрачности, анимации |
| AVIF | На 50-70% меньше | 92% (Chrome, Firefox, Safari 16+) | Лучшее качество, медленнее конвертация |
Оптимальная стратегия: отдавать AVIF поддерживающим браузерам, WebP — остальным современным, JPEG — совсем старым (IE11, старые Android). Nginx умеет это делать автоматически через проверку заголовка Accept.
Шаг 1: Проверка поддержки модулей в Nginx
Для автоматической конвертации нужны модули ngx_http_image_filter_module (встроен в большинство сборок) и внешние инструменты конвертации. Проверяем наличие модулей:
nginx -V 2>&1 | grep -o with-http_image_filter_moduleЕсли модуль отсутствует, его нужно установить. Для Ubuntu/Debian:
sudo apt install nginx-extrasПакет nginx-extras включает все дополнительные модули, включая image_filter.
Шаг 2: Установка инструментов конвертации
Nginx сам не конвертирует изображения — он вызывает внешние утилиты. Устанавливаем cwebp (для WebP) и avifenc (для AVIF):
# Установка WebP tools
sudo apt install webp
# Установка AVIF tools (libavif)
sudo apt install libavif-bin
# Проверка установки
cwebp -version
avifenc --versionЕсли libavif-bin недоступен в репозиториях (для старых Ubuntu), устанавливаем через snap или компилируем из исходников. Альтернатива — использовать ImageMagick, который поддерживает оба формата:
sudo apt install imagemagick
convert -version | grep -i "avif\|webp"Шаг 3: Настройка Nginx для автоматической конвертации
Создаём отдельный location-блок для обработки изображений. Открываем конфиг сайта /etc/nginx/sites-available/wordpress и добавляем перед основным location /:
# Кэш для конвертированных изображений
proxy_cache_path /var/cache/nginx/images levels=1:2 keys_zone=images_cache:10m max_size=10g inactive=60m;
# Обработка изображений с автоматической конвертацией
location ~* \.(jpe?g|png)$ {
# Проверяем поддержку AVIF браузером
set $avif_support 0;
if ($http_accept ~* "avif") {
set $avif_support 1;
}
# Проверяем поддержку WebP
set $webp_support 0;
if ($http_accept ~* "webp") {
set $webp_support 1;
}
# Приоритет: AVIF > WebP > оригинал
set $image_format "original";
if ($webp_support = 1) {
set $image_format "webp";
}
if ($avif_support = 1) {
set $image_format "avif";
}
# Пробуем отдать конвертированную версию из кэша
try_files /wp-content/uploads-cache/$image_format$uri.$image_format @convert_$image_format;
# Заголовки для кэширования
expires 30d;
add_header Cache-Control "public, immutable";
add_header Vary "Accept";
}
# Конвертация в AVIF
location @convert_avif {
alias /var/www/wordpress;
# Вызываем скрипт конвертации
content_by_lua_block {
local uri = ngx.var.uri
local source = "/var/www/wordpress" .. uri
local cache_dir = "/var/www/wordpress/wp-content/uploads-cache/avif"
local dest_dir = cache_dir .. string.match(uri, "(.*/)")
local dest = cache_dir .. uri .. ".avif"
-- Создаём директорию, если не существует
os.execute("mkdir -p " .. dest_dir)
-- Конвертируем через avifenc (качество 65, скорость 6)
local cmd = "avifenc --min 20 --max 30 --speed 6 " .. source .. " " .. dest .. " 2>/dev/null"
os.execute(cmd)
-- Если конвертация успешна, отдаём файл
if os.execute("test -f " .. dest) then
ngx.header["Content-Type"] = "image/avif"
ngx.say(ngx.location.capture("/internal_avif" .. uri .. ".avif").body)
else
-- Fallback на оригинал
ngx.exec("@serve_original")
end
}
}
# Конвертация в WebP
location @convert_webp {
alias /var/www/wordpress;
content_by_lua_block {
local uri = ngx.var.uri
local source = "/var/www/wordpress" .. uri
local cache_dir = "/var/www/wordpress/wp-content/uploads-cache/webp"
local dest_dir = cache_dir .. string.match(uri, "(.*/)")
local dest = cache_dir .. uri .. ".webp"
os.execute("mkdir -p " .. dest_dir)
-- Конвертируем через cwebp (качество 80)
local cmd = "cwebp -q 80 " .. source .. " -o " .. dest .. " 2>/dev/null"
os.execute(cmd)
if os.execute("test -f " .. dest) then
ngx.header["Content-Type"] = "image/webp"
ngx.say(ngx.location.capture("/internal_webp" .. uri .. ".webp").body)
else
ngx.exec("@serve_original")
end
}
}
# Отдача оригинала (fallback)
location @serve_original {
alias /var/www/wordpress;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Внутренние location для отдачи конвертированных файлов
location ~* ^/internal_(avif|webp)(.*\.(avif|webp))$ {
internal;
alias /var/www/wordpress/wp-content/uploads-cache/$1$2;
}Конфиг использует Lua-модуль (lua-nginx-module), который встроен в nginx-extras. Если Lua недоступен, можно использовать альтернативный подход через image_filter или внешние скрипты.
Шаг 4: Альтернативный метод без Lua (через image_filter)
Если Lua-модуль не установлен или не работает, используется встроенный image_filter для конвертации. Этот метод проще, но менее гибкий:
# Простая конвертация через image_filter (только WebP)
location ~* \.(jpe?g|png)$ {
set $webp "";
# Проверяем поддержку WebP
if ($http_accept ~* "webp") {
set $webp "1";
}
# Если WebP поддерживается и файл существует в кэше
if (-f $request_filename.webp) {
set $webp "${webp}2";
}
# Отдаём WebP версию
if ($webp = "12") {
rewrite ^(.*)$ $1.webp break;
}
# Иначе конвертируем на лету
image_filter convert webp;
image_filter_jpeg_quality 80;
image_filter_buffer 10M;
# Кэшируем результат
image_filter_store /var/www/wordpress/wp-content/uploads-cache$uri.webp;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Vary "Accept";
}Этот метод проще, но не поддерживает AVIF и менее эффективен для больших изображений.
Шаг 5: Предварительная конвертация существующих изображений
Конвертация на лету при первом запросе создаёт задержку для пользователя. Чтобы этого избежать, конвертируем все существующие изображения заранее через bash-скрипт:
#!/bin/bash
# /usr/local/bin/convert-images.sh
UPLOADS_DIR="/var/www/wordpress/wp-content/uploads"
CACHE_DIR="/var/www/wordpress/wp-content/uploads-cache"
# Создаём структуру кэша
mkdir -p "$CACHE_DIR/webp"
mkdir -p "$CACHE_DIR/avif"
# Находим все JPEG и PNG файлы
find "$UPLOADS_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | while read file; do
# Вычисляем относительный путь
rel_path="${file#$UPLOADS_DIR}"
# Конвертируем в WebP
webp_dest="$CACHE_DIR/webp$rel_path.webp"
mkdir -p "$(dirname "$webp_dest")"
if [ ! -f "$webp_dest" ]; then
cwebp -q 80 "$file" -o "$webp_dest"
echo "WebP: $rel_path"
fi
# Конвертируем в AVIF
avif_dest="$CACHE_DIR/avif$rel_path.avif"
mkdir -p "$(dirname "$avif_dest")"
if [ ! -f "$avif_dest" ]; then
avifenc --min 20 --max 30 --speed 6 "$file" "$avif_dest" 2>/dev/null
echo "AVIF: $rel_path"
fi
done
echo "Конвертация завершена"Делаем скрипт исполняемым и запускаем:
sudo chmod +x /usr/local/bin/convert-images.sh
sudo /usr/local/bin/convert-images.shДля сайтов с тысячами изображений процесс может занять несколько часов. Запускаем в screen или tmux, чтобы скрипт не прервался при закрытии SSH-сессии.
Шаг 6: Автоматическая конвертация новых загрузок
Чтобы новые изображения, загружаемые через WordPress, автоматически конвертировались, настраиваем inotify для отслеживания изменений в папке uploads:
# Установка inotify-tools
sudo apt install inotify-toolsСоздаём systemd-сервис для постоянного мониторинга:
# /etc/systemd/system/image-watcher.service
[Unit]
Description=Watch WordPress uploads for new images
After=network.target
[Service]
Type=simple
User=www-data
ExecStart=/usr/local/bin/watch-uploads.sh
Restart=always
[Install]
WantedBy=multi-user.targetСоздаём скрипт мониторинга:
#!/bin/bash
# /usr/local/bin/watch-uploads.sh
UPLOADS_DIR="/var/www/wordpress/wp-content/uploads"
CACHE_DIR="/var/www/wordpress/wp-content/uploads-cache"
inotifywait -m -r -e create -e moved_to --format '%w%f' "$UPLOADS_DIR" | while read file; do
# Проверяем, что это изображение
if [[ "$file" =~ \.(jpe?g|png)$ ]]; then
rel_path="${file#$UPLOADS_DIR}"
# Конвертируем в WebP
webp_dest="$CACHE_DIR/webp$rel_path.webp"
mkdir -p "$(dirname "$webp_dest")"
cwebp -q 80 "$file" -o "$webp_dest" 2>/dev/null
# Конвертируем в AVIF
avif_dest="$CACHE_DIR/avif$rel_path.avif"
mkdir -p "$(dirname "$avif_dest")"
avifenc --min 20 --max 30 --speed 6 "$file" "$avif_dest" 2>/dev/null
echo "Converted: $rel_path"
fi
doneВключаем и запускаем сервис:
sudo chmod +x /usr/local/bin/watch-uploads.sh
sudo systemctl daemon-reload
sudo systemctl enable image-watcher
sudo systemctl start image-watcherТеперь при каждой загрузке изображения через WordPress оно автоматически конвертируется в оба формата в течение нескольких секунд.
Шаг 7: Отключение плагинов конвертации в WordPress
Если раньше использовались плагины вроде ShortPixel, Imagify или EWWW Image Optimizer, их нужно деактивировать, чтобы избежать двойной конвертации и конфликтов:
# Через WP-CLI
wp plugin deactivate shortpixel-image-optimiser
wp plugin deactivate imagify
wp plugin deactivate ewww-image-optimizerТакже отключаем встроенную в WordPress 5.8+ автоматическую конвертацию в WebP (если она включена), чтобы не создавать дубликаты:
// В functions.php темы
add_filter( 'image_editor_output_format', function( $formats ) {
// Отключаем автоматическую конвертацию WordPress
$formats['image/jpeg'] = 'image/jpeg';
$formats['image/png'] = 'image/png';
return $formats;
} );Шаг 8: Настройка кэширования и очистки
Конвертированные изображения занимают место на диске. Настраиваем автоматическую очистку старых файлов и удаление кэша при удалении оригиналов.
Очистка старых файлов кэша:
Добавляем в cron задачу для удаления конвертированных версий удалённых оригиналов:
#!/bin/bash
# /usr/local/bin/clean-image-cache.sh
UPLOADS_DIR="/var/www/wordpress/wp-content/uploads"
CACHE_DIR="/var/www/wordpress/wp-content/uploads-cache"
# Находим все файлы в кэше
find "$CACHE_DIR" -type f \( -name "*.webp" -o -name "*.avif" \) | while read cached_file; do
# Вычисляем путь к оригиналу
rel_path="${cached_file#$CACHE_DIR/webp}"
rel_path="${rel_path%.webp}"
rel_path="${rel_path%.avif}"
original="$UPLOADS_DIR$rel_path"
# Если оригинал не существует, удаляем кэш
if [ ! -f "$original" ]; then
rm -f "$cached_file"
echo "Removed: $cached_file"
fi
doneДобавляем в cron для еженедельного запуска:
0 3 * * 0 /usr/local/bin/clean-image-cache.shОчистка при удалении медиафайла через WordPress:
Добавляем хук в functions.php для удаления кэша при удалении изображения из медиатеки:
add_action( 'delete_attachment', function( $post_id ) {
$file = get_attached_file( $post_id );
$upload_dir = wp_upload_dir();
$rel_path = str_replace( $upload_dir['basedir'], '', $file );
$cache_dir = $upload_dir['basedir'] . '/uploads-cache';
// Удаляем WebP версию
$webp_file = $cache_dir . '/webp' . $rel_path . '.webp';
if ( file_exists( $webp_file ) ) {
unlink( $webp_file );
}
// Удаляем AVIF версию
$avif_file = $cache_dir . '/avif' . $rel_path . '.avif';
if ( file_exists( $avif_file ) ) {
unlink( $avif_file );
}
} );Шаг 9: Мониторинг экономии трафика
Чтобы оценить эффективность конвертации, анализируем размеры файлов и трафик:
# Сравнение размеров оригиналов и конвертированных версий
echo "=== Статистика конвертации ==="
original_size=$(du -sh /var/www/wordpress/wp-content/uploads | awk '{print $1}')
webp_size=$(du -sh /var/www/wordpress/wp-content/uploads-cache/webp | awk '{print $1}')
avif_size=$(du -sh /var/www/wordpress/wp-content/uploads-cache/avif | awk '{print $1}')
echo "Оригиналы: $original_size"
echo "WebP: $webp_size"
echo "AVIF: $avif_size"
# Подсчёт средней экономии
find /var/www/wordpress/wp-content/uploads -type f \( -name "*.jpg" -o -name "*.jpeg" \) | head -100 | while read file; do
rel_path="${file#/var/www/wordpress/wp-content/uploads}"
webp_file="/var/www/wordpress/wp-content/uploads-cache/webp$rel_path.webp"
avif_file="/var/www/wordpress/wp-content/uploads-cache/avif$rel_path.avif"
if [ -f "$webp_file" ] && [ -f "$avif_file" ]; then
orig_size=$(stat -c%s "$file")
webp_size=$(stat -c%s "$webp_file")
avif_size=$(stat -c%s "$avif_file")
webp_save=$((100 - (webp_size * 100 / orig_size)))
avif_save=$((100 - (avif_size * 100 / orig_size)))
echo "$rel_path: WebP -${webp_save}%, AVIF -${avif_save}%"
fi
doneТипичная экономия: WebP — 25-40%, AVIF — 50-70% от размера оригинального JPEG.
Шаг 10: Интеграция с CDN (Cloudflare, BunnyCDN)
Если используется CDN, нужно убедиться, что он корректно кэширует разные форматы изображений для разных браузеров. Критично правильный заголовок Vary: Accept, который уже добавлен в конфиг Nginx.
Настройка Cloudflare:
В Cloudflare включаем Polish (автоматическая конвертация в WebP) только если не используется собственная конвертация через Nginx, чтобы избежать двойной обработки. Если Nginx уже конвертирует изображения, Polish нужно отключить.
Настройка Page Rules:
Для корректного кэширования разных форматов создаём Page Rule:
# URL: custom-code.ru/wp-content/uploads/*
# Settings:
# - Cache Level: Cache Everything
# - Edge Cache TTL: 1 month
# - Browser Cache TTL: 1 yearЧек-лист настройки конвертации изображений
- ✅ Установлены инструменты
cwebpиavifenc - ✅ Nginx настроен для автоматической конвертации с fallback
- ✅ Существующие изображения конвертированы через скрипт
- ✅ Настроен inotify для автоматической конвертации новых загрузок
- ✅ Плагины конвертации в WordPress деактивированы
- ✅ Встроенная конвертация WordPress отключена через фильтр
- ✅ Настроена автоматическая очистка кэша
- ✅ CDN настроен для корректного кэширования с
Vary: Accept - ✅ Экономия трафика измерена и задокументирована
Решение частых проблем
Изображения не конвертируются (отдаётся оригинал)
Проверяем логи Nginx: sudo tail -f /var/log/nginx/error.log. Частые причины:
- Недостаточно прав у Nginx на запись в
uploads-cache— выполняемsudo chown -R www-data:www-data /var/www/wordpress/wp-content/uploads-cache - Не установлены
cwebpилиavifenc— проверяем черезwhich cwebp - Lua-модуль не загружен — проверяем через
nginx -V | grep lua
Конвертация слишком медленная
AVIF конвертируется в 10-50 раз медленнее WebP. Для больших изображений (больше 2 МБ) процесс может занимать 10-30 секунд. Решения:
- Увеличиваем параметр
--speedвavifenc(от 0 до 10, где 10 — быстрее, но хуже качество) - Конвертируем только изображения меньше определённого размера
- Используем предварительную конвертацию через скрипт вместо on-the-fly
Браузер не получает WebP/AVIF
Проверяем заголовки ответа через DevTools (Network → изображение → Response Headers). Должны быть:
Content-Type: image/webpилиimage/avifVary: Accept
Если отдаётся image/jpeg, значит Nginx не распознал поддержку формата. Проверяем, что браузер отправляет заголовок Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8.
CDN кэширует только JPEG для всех браузеров
Проблема в отсутствии заголовка Vary: Accept. CDN должен кэшировать разные версии для разных значений Accept. Убеждаемся, что заголовок добавлен в конфиг Nginx и не удаляется CDN. В Cloudflare проверяем настройки Caching → Configuration → Browser Cache TTL.
После конвертации изображения выглядят хуже
Увеличиваем качество конвертации. Для WebP меняем -q 80 на -q 90. Для AVIF меняем --min 20 --max 30 на --min 30 --max 40. Это увеличит размер файлов, но улучшит качество.
Итог
Автоматическая конвертация изображений через Nginx — наиболее эффективный способ оптимизации медиафайлов в WordPress. Конвертация происходит один раз, результаты кэшируются, а WordPress продолжает работать как обычно, не зная о процессе. Комбинация WebP и AVIF с правильным fallback даёт экономию трафика до 70%, что напрямую улучшает метрики Core Web Vitals и скорость загрузки сайта. В сочетании с FastCGI Cache и Redis Object Cache это создаёт полноценный стек оптимизации для высоконагруженных WordPress-проектов.