Сжатие ответов — самый дешёвый способ ускорить сайт: HTML, CSS и JavaScript по сети занимают в несколько раз меньше места, а сервер тратит на это немного процессорного времени. В Nginx есть встроенный gzip и подключаемый brotli. Настроить их несложно, но реальная выгода зависит от типов файлов, уровня сжатия, кэширования и того, отдаёт ли сервер файлы сам или проксирует приложение. Ниже — рабочая конфигурация и проверка результата.
Как Nginx решает, сжимать ли ответ
Клиент присылает заголовок Accept-Encoding со списком поддерживаемых алгоритмов. Nginx смотрит на MIME-тип ответа, его размер и настройки модуля. Если тип не перечислен в gzip_types, ответ уйдёт без сжатия — даже при включённом gzip on. По умолчанию модуль сжимает только text/html; остальное нужно добавлять явно.
Сжатый ответ помечается заголовком Content-Encoding: gzip или br. При этом меняется тело, но не URL, поэтому кэши и прокси должны различать варианты по Vary: Accept-Encoding. Если заголовка нет, промежуточный кэш может отдать gzip-версию клиенту, который её не понимает.
Есть два режима: сжатие на лету для динамики и отдача заранее подготовленных файлов, которая экономит процессор и позволяет использовать максимальный уровень brotli.
Базовая настройка gzip
Проверенную конфигурацию удобно держать в отдельном файле и подключать из блока http или конкретного server:
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml image/svg+xml;
gzip_static on;
gzip_comp_level 6 — разумный компромисс: уровень 9 даёт единицы процентов выигрыша, но заметно нагружает процессор на больших файлах. gzip_min_length 1024 отсекает мелкие ответы, где служебные заголовки съедают всю экономию. gzip_proxied any нужен, если перед Nginx стоит прокси или балансировщик и запросы приходят с заголовком Via. gzip_static on включает отдачу готовых файлов .gz, если они лежат рядом с оригиналом; соответствующий модуль должен быть собран в бинарнике.
После правки проверяем синтаксис и перезагружаем сервер без разрыва соединений:
nginx -t
systemctl reload nginx
curl -sI -H 'Accept-Encoding: gzip' https://example.com/ | grep -i content-encoding
Brotli: модуль, которого нет в официальной сборке
Brotli обычно сжимает текстовые ответы на 10–20% сильнее gzip при сопоставимом времени, а на статических файлах с уровнем 11 разница бывает ещё заметнее. Но ngx_brotli не входит в официальные сборки Nginx. Сначала смотрим, что уже есть в системе:
nginx -V 2>&1 | tr ' ' '\n' | grep -i brotli
ls /usr/lib/nginx/modules/ | grep -i brotli
Если модуль поставляется пакетом дистрибутива, достаточно установить пакет и подключить модуль; путь к .so уточните в файлах дистрибутива. Если пакета нет, модуль собирают под ту же версию Nginx с флагом --with-compat; неподходящий бинарный модуль приведёт к ошибке загрузки. Типовая настройка выглядит так:
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types text/plain text/css application/json application/javascript
application/xml image/svg+xml;
brotli_static on;
Уровень 5 для динамики — безопасный старт. Уровни 9–11 имеют смысл только для предварительно сжатых файлов: на каждый запрос такой компрессор тратит слишком много процессорного времени. Nginx выбирает brotli, если клиент прислал br в Accept-Encoding, иначе отдаёт gzip или несжатый ответ.
Предварительное сжатие статики
Самый предсказуемый вариант для CSS, JS и SVG — заранее создать .gz и .br рядом с исходниками и включить gzip_static и brotli_static. Nginx не будет сжимать ничего на лету, а отдаст готовый файл. Сборку удобно делать в CI или в скрипте деплоя:
find public/assets -type f \( -name '*.css' -o -name '*.js' -o -name '*.svg' \) -exec gzip -9 -k -f {} \;
find public/assets -type f \( -name '*.css' -o -name '*.js' -o -name '*.svg' \) -exec brotli -q 11 -k -f {} \;
ls -lh public/assets/app.css*
При обновлении файла пересобирайте оба архива. Для сайта на CDN убедитесь, что он не сжимает ответ повторно и умеет отдавать .br с корректным Content-Type.
Что сжимать не нужно
- JPEG, PNG, WebP, AVIF и GIF — они уже сжаты, повторная обработка только тратит ресурсы.
- Видео, аудио, ZIP, PDF и другие контейнеры с собственным сжатием.
- Шрифты WOFF2: формат уже использует сжатие, выигрыш близок к нулю.
- Очень маленькие ответы — меньше 1 КБ, если только это не критичный JSON.
Текстовые форматы — HTML, CSS, JS, JSON, XML, SVG и plain text — сжимаются хорошо, часто на 60–80%.
Как измерить реальную выгоду
Заголовки показывают только факт сжатия, но не размер и время. Сравним три варианта одним и тем же запросом:
for enc in identity gzip br; do
curl -s -o /dev/null -w "$enc: %{size_download} bytes, %{time_total} s\n" \
-H "Accept-Encoding: $enc" https://example.com/assets/app.js
done
size_download — это переданные байты без распаковки, поэтому цифры можно сравнивать напрямую. Для честной оценки повторите замер несколько раз и учитывайте кэш: второй запрос может прийти из CDN и не отражать работу Nginx. Полезно также посмотреть Content-Length и наличие Vary:
curl -sI -H 'Accept-Encoding: gzip, br' https://example.com/assets/app.js \
| grep -iE 'content-encoding|content-length|vary|cache-control'
Типичные ошибки
- Нет
Vary. Кэш отдаёт сжатый вариант клиенту без поддержки gzip. Лечитсяgzip_vary onи аналогичной настройкой на CDN. - Двойное сжатие. Nginx сжимает ответ, а CDN или приложение — ещё раз. Проверяйте цепочку заголовков и отключайте лишний слой.
- Уровень 9 на динамике. При росте нагрузки время ответа растёт быстрее, чем падает трафик. Для динамических страниц достаточно 5–6.
- Сжатие уже сжатых файлов. Процессор тратится, размер почти не меняется.
- Забытый
gzip_proxied. За балансировщиком ответы внезапно перестают сжиматься. - Отсутствие проверки после деплоя. Обновлённый конфиг может не примениться из-за ошибки в другом файле;
nginx -tобязателен.
Итог
Для большинства сайтов достаточно gzip с уровнем 5–6 и предварительного сжатия статики. Brotli даёт заметную экономию трафика, но требует отдельного модуля и аккуратной сборки архивов; на динамике не стоит выставлять максимальный уровень. Проверяйте не факт заголовка, а размер, время и поведение кэша. Тогда сжатие остаётся дешёвой оптимизацией, а не источником загадочных ошибок.
Минимальный чек-лист: nginx -t проходит, Vary присутствует, статика отдаётся как .br или .gz, уровни сжатия разумны, а после деплоя вы повторили замеры через curl.