Представь типичную страницу: HTML на 80 КБ, CSS на 200 КБ, бандл JavaScript на 600 КБ, плюс JSON-ответы API. Всё это текст, а текст сжимается отлично - в 4-8 раз. Если ты отдаёшь это сырьём, клиент с мобильным интернетом тянет почти мегабайт, твой канал на аплинке прогревается, а время до первого отрисованного пикселя растёт линейно с размером. Включаешь сжатие - и те же 600 КБ JS улетают в провод как 130 КБ. Клиент получает их быстрее, ты экономишь трафик (а исходящий трафик у облачных провайдеров стоит денег), а пользователь видит страницу раньше.
Сжатие на шлюзе - это сделка: ты тратишь CPU нгинкса, чтобы сэкономить байты в сети. Сделка почти всегда выгодная, потому что сеть медленнее процессора на порядки, особенно для далёкого или мобильного клиента. Весь вопрос - как настроить эту сделку, чтобы не спалить CPU зря и не сломать кэш. Тема nginx сжатие звучит просто, но именно здесь новички теряют половину выигрыша на дефолтных значениях и кривом списке типов.
Важно сразу разделить два режима. Динамическое сжатие - nginx жмёт ответ на лету, в момент отдачи. Это работает для всего: и для статики, и для проксированного ответа от бэкенда. Предсжатие - ты заранее положил рядом с файлом его сжатую копию (style.css и style.css.gz), и nginx просто отдаёт готовый файл, не тратя CPU вообще. Предсжатие выгоднее по нагрузке, но годится только для статики, которую ты контролируешь заранее.

nginx gzip: базовая настройка и каждая директива
gzip - встроенный модуль, есть в любой сборке nginx, Angie и OpenResty из коробки. Это твой минимум, который понимают вообще все клиенты с 2000-х годов. Базовый блок nginx gzip настройка выглядит так - именно в таком виде он лежит в эталонном проекте как globals/gzip.conf:
Код: Выделить всё
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml
application/rss+xml
image/svg+xml;
gzip on - включает сжатие на лету. По умолчанию off. Можно включить на уровне http (глобально), а в отдельном location выключить gzip off, если там сжимать не нужно.
gzip_comp_level 5 - уровень от 1 до 9. Это главный регулятор сделки CPU против размера. Уровень 1 жмёт быстро и слабо, 9 - медленно и сильно. Ловушка в том, что зависимость нелинейная: разница в размере между 5 и 9 обычно копеечная (проценты), а разница в потраченном CPU - заметная. Поэтому эталон 5 - это золотая середина, дальше расти смысла мало. Не ставь 9 рефлекторно: на загруженном шлюзе ты сожжёшь процессор ради лишнего процента экономии. Если жмёшь статику один раз и кэшируешь - другое дело, но для динамики 5 это правильный дефолт.
gzip_min_length 1024 - не жать ответы короче 1 КБ. Причина простая: у gzip есть постоянные накладные расходы (заголовок, словарь, контрольная сумма), и крошечный ответ после сжатия может стать больше оригинала, а CPU ты потратишь зря. Берётся из заголовка Content-Length, поэтому работает только когда длина известна заранее. Для chunked-ответов без Content-Length эта проверка не сработает.
gzip_vary on - добавляет в ответ заголовок Vary: Accept-Encoding. Это критично для кэшей. Он говорит любому промежуточному кэшу (CDN, прокси, браузерный кэш): этот ответ зависит от того, какое сжатие просил клиент, храните версии раздельно. Без него ты рискуешь отдать сжатый ответ клиенту, который сжатие не понимает, или наоборот. Никогда не выключай gzip_vary, если перед тобой есть хоть какой-то кэш.
gzip_proxied any - по умолчанию nginx не жмёт ответы на проксированные запросы (когда в запросе есть заголовок Via). Логика старая: мол, раз запрос пришёл через прокси, может, тот прокси уже разберётся. На современном шлюзе это мешает - ты хочешь жать всегда, поэтому any означает "жми независимо от прокси-заголовков". Есть и тонкие значения (expired, no-cache, no-store, private, auth) если нужно жать выборочно по cache-control бэкенда, но any - честный дефолт для gateway.
gzip_types - вот где теряют выигрыш чаще всего. По умолчанию nginx жмёт ТОЛЬКО text/html и больше ничего. То есть из коробки твои CSS, JS и JSON не сжимаются вообще. Поэтому nginx gzip_types надо задавать явно и перечислять все текстовые MIME. text/html добавлять не нужно - он всегда в списке неявно. Обрати внимание на application/json (ответы API), application/javascript (бандлы), image/svg+xml (это XML-текст, жмётся прекрасно, несмотря на префикс image). А вот добавлять сюда image/png или video/mp4 - грубая ошибка, об этом ниже.
Ещё есть gzip_disable - регэксп по User-Agent, чтобы выключить gzip для сломанных клиентов. Исторически это был костыль gzip_disable "msie6" против древнего Internet Explorer 6, который криво обрабатывал сжатие. В 2026 это реликт - живых IE6 нет, и директива тебе, скорее всего, не нужна вообще. Знай, что она есть, но не копируй её из старых гайдов бездумно.
Предсжатые файлы: gzip_static
Динамика хороша, но зачем жать один и тот же style.css на каждый запрос? Директива gzip_static решает это для статики:
Код: Выделить всё
location ~* \.(css|js|svg)$ {
gzip_static on;
expires 30d;
add_header Cache-Control "public";
}
nginx brotli: сторонний модуль, но лучше gzip для текста
Brotli - алгоритм от Google, который для текста стабильно даёт на 15-25% меньший размер, чем gzip на сопоставимом уровне. Секрет - встроенный статический словарь из частых веб-фрагментов (тегов, ключевых слов), так что HTML и CSS сжимаются особенно хорошо. Все современные браузеры понимают br уже много лет.
Главное, что надо чётко зафиксировать: nginx brotli - это СТОРОННИЙ модуль ngx_brotli, его НЕТ в ванильном nginx. Это не встроенная фича, как gzip. Его нужно собрать или поставить отдельным пакетом как динамический модуль (.so) и подключить через load_module в начале конфига, до блока http:
Код: Выделить всё
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
image/svg+xml;
brotli_static on;
Образ openresty/openresty, на котором стоит эталонный проект, brotli из коробки не несёт - его нужно добавлять отдельно. Поэтому в эталоне globals/gzip.conf использует именно gzip как универсальную базу, которая работает везде без сборки модулей. Brotli - это апгрейд, который ты ставишь осознанно, когда готов собрать модуль.
nginx zstd: нативно только в Angie
Zstandard (zstd) - алгоритм от Facebook, который славится отличным балансом: высокая скорость сжатия И декомпрессии при степени сжатия на уровне gzip или лучше. Для API-gateway с большими JSON-ответами это интересный кандидат.
И вот ключевой пункт аудита, который надо запомнить намертво. Нативный nginx zstd есть ТОЛЬКО в Angie - российском форке nginx. В Angie это полноценная встроенная фича с директивами zstd on и zstd_static, оформленная так же, как gzip:
Код: Выделить всё
zstd on;
zstd_comp_level 6;
zstd_min_length 256;
zstd_types
text/css
application/json
application/javascript;
zstd_static on;
Что сжимать, а что категорически нет
Сжатие имеет смысл только для данных с избыточностью - то есть для текста. Бинарные форматы, которые УЖЕ сжаты внутри себя, повторно жать бессмысленно и вредно.
- Жать: HTML, CSS, JS, JSON, XML, обычный текст, SVG (это XML), шрифтовые форматы в текстовом виде. Всё, что человекочитаемо или структурно повторяется.
- НЕ жать: JPEG, PNG, WebP, AVIF (уже сжаты), MP4, WebM (видео сжато кодеком), MP3, AAC, ZIP, gzip-архивы, и шрифты woff2 (внутри woff2 уже brotli).
Отдельно про woff2: это распространённая ошибка - люди видят шрифт и думают "пусть жмётся". Внутри woff2 уже лежит brotli-сжатие, так что повторное gzip только мешает. Не включай его в типы.
Проверка: как убедиться, что сжатие реально работает
Не верь конфигу на слово - проверяй curl. Клиент сообщает серверу поддерживаемые алгоритмы заголовком Accept-Encoding, а сервер отвечает выбранным алгоритмом в Content-Encoding. Запрашиваем заведомо текстовый ресурс и смотрим заголовки:
Код: Выделить всё
curl -s -I -H 'Accept-Encoding: br,gzip,zstd' https://example.com/app.js
Код: Выделить всё
HTTP/2 200
content-type: application/javascript
content-encoding: gzip
vary: Accept-Encoding
Чтобы увидеть реальную экономию, сравни размеры со сжатием и без:
Код: Выделить всё
# без сжатия - сырой размер
curl -s -o /dev/null -w '%{size_download}\n' https://example.com/app.js
# со сжатием
curl -s -o /dev/null -H 'Accept-Encoding: gzip' \
-w '%{size_download}\n' https://example.com/app.js
Тонкость с приоритетом: если у тебя включены и gzip, и brotli, nginx выберет алгоритм по своей логике, а не строго по порядку в Accept-Encoding. Чтобы проверить именно brotli, проси только его: -H 'Accept-Encoding: br' и смотри content-encoding: br.
Связка с кэшем: почему Vary решает всё
Это место, где ломаются продакшены. Допустим, перед nginx стоит CDN или включён proxy_cache. Первым пришёл клиент с Accept-Encoding: gzip, получил сжатый ответ, и кэш его сохранил. Следующим пришёл старый клиент или curl без сжатия - и кэш отдаёт ему сжатый gzip-мусор, который тот не умеет распаковывать. Страница ломается.
Спасает ровно gzip_vary on (и brotli добавляет Vary сам). Заголовок Vary: Accept-Encoding заставляет кэш хранить отдельные записи под каждый вариант кодирования. Поэтому правило железное: где есть кэш - там обязателен Vary. В эталонном проекте gzip_vary on стоит глобально, и это не случайность, а защита кэш-слоя.
Типичные грабли
- Пустой gzip_types. Самая частая. Включил gzip on, обрадовался, а жмётся только HTML. CSS и JS летят сырыми. Всегда задавай полный список типов.
- gzip_comp_level 9 на проде. Сжёг CPU ради процента. Держи 5 для динамики.
- Сжатие уже сжатого. image/jpeg или woff2 в типах - чистый проигрыш по CPU.
- Нет Vary при кэше. Битые ответы клиентам, которые не поняли кодирование.
- Думать, что brotli/zstd встроены в nginx. brotli - сторонний модуль всегда; zstd - нативен только в Angie. Ставишь load_module и собираешь модуль, иначе конфиг не стартует с ошибкой "unknown directive".
- gzip_min_length не работает на chunked. Если бэкенд отдаёт без Content-Length, проверка длины не сработает, и крошечные ответы тоже пожмутся. Учитывай для стриминговых эндпоинтов.
- Двойное сжатие от бэкенда. Если приложение уже отдаёт Content-Encoding: gzip, nginx не будет жать повторно - но если бэкенд жмёт, а ты ждёшь brotli от nginx, ты получишь только gzip. Решай, кто отвечает за сжатие - бэкенд или шлюз, обычно шлюз.
Повтори по шагам на тестовом nginx или в Docker с openresty/openresty.
- Шаг 1. Создай тестовый файл, который заведомо стоит сжать: положи в корень сайта app.js размером хотя бы пару КБ повторяющегося текста (можно сгенерить: yes 'console.log("hello world");' | head -2000 > app.js).
- Шаг 2. Подними nginx БЕЗ настройки gzip. Запроси curl -s -I -H 'Accept-Encoding: gzip' http://localhost/app.js - убедись, что строки content-encoding НЕТ.
- Шаг 3. Добавь в http блок: gzip on; gzip_types application/javascript; gzip_min_length 1024; gzip_vary on;. Проверь конфиг nginx -t и перезагрузи.
- Шаг 4. Повтори curl. Теперь в ответе должны появиться content-encoding: gzip и vary: Accept-Encoding.
- Шаг 5. Сравни размеры через -w '%{size_download}' с заголовком Accept-Encoding: gzip и без него. Зафиксируй коэффициент.
- Шаг 6 (если есть модуль). Подключи ngx_brotli через load_module, добавь brotli on и тот же тип. Запроси с -H 'Accept-Encoding: br' и сравни размер br против gzip - brotli должен дать меньше.
- Шаг 7. Добавь image/jpeg в gzip_types, положи рядом jpg, запроси его со сжатием и убедись, что размер НЕ уменьшился - так ты руками увидишь, почему сжатые форматы жать нельзя.
- Почему gzip_comp_level 9 редко имеет смысл на проде, и какой уровень берут эталоном?
- Что произойдёт с кэшем, если включить gzip, но забыть gzip_vary on?
- В чём принципиальная разница доступности zstd между ванильным nginx и Angie? А что насчёт brotli?
- Почему добавление image/png или woff2 в gzip_types - это вредно, а не просто бесполезно?
Сжатие - дешёвый способ ускорить отдачу и срезать трафик, если настроить его головой. gzip - твоя универсальная база, встроенная везде: comp_level 5, полный список текстовых gzip_types, min_length 1024, vary on, proxied any - ровно как в эталонном globals/gzip.conf. brotli даёт лучший результат для текста, но это всегда сторонний модуль. zstd нативен только в Angie, в nginx - тоже сторонний. Жми текст, не трогай уже сжатые форматы, держи Vary при кэше и проверяй всё через curl с Accept-Encoding. Где можешь - предсжимай статику через gzip_static/brotli_static и отдавай готовые файлы с нулевой нагрузкой на CPU.