gzip, Brotli, Zstd HTTP сжатие для разработчиков, которые случайно устанавливают content-encoding: identity

Обновлено

Как работает согласование сжатия HTTP (Accept-Encoding / Content-Encoding), сравнительный анализ gzip, Brotli и Zstd, как проверить, что сжатие действительно включено с помощью curl, и четыре неправильной настройки, которые молча отключают его.

gzip, Brotli, Zstd: HTTP сжатие для разработчиков, которые случайно устанавливают content-encoding: identity 1
Реклама · УДАЛИТЬ?

Ваша конфигурация nginx имеет gzip on;. Ваше приложение возвращает JSON. Тело ответа всё ещё несжато и составляет 35 КБ. Нет ошибок, нет предупреждений — сжатие просто не происходит молча.

Это обычно одна из четырёх ошибок настройки. Но сначала: как работает переговоры.

Как работает переговоры по сжатию HTTP

Два заголовка, ничего больше. Клиент объявляет, какие типы сжатия он может декодировать в Accept-Encoding. Сервер выбирает алгоритм, сжимает тело и объявляет свой выбор в Content-Encoding:

GET /api/data HTTP/1.1
Host: example.com
Accept-Encoding: gzip, deflate, br, zstd
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Vary: Accept-Encoding

The Vary: Accept-Encoding заголовок является необязательным, если вы хотите, чтобы CDN корректно кэшировал ответы. Без него CDN может кэшировать сжатый ответ Brotli и подавать его клиенту, который только объявил gzip в Accept-Encoding. Тот клиент затем пытается декодировать Brotli как gzip и получает мусор. nginx добавляет это автоматически. gzip_vary on; является технически корректным — оно означает «без сжатия» — но никто не устанавливает его явно. Реальный сценарий сбоя — наоборот: отсутствие заголовка

Content-Encoding: identity всех, когда вы ожидаете его присутствия. Content-Encoding заголовок в любом случае, когда вы ожидали его.

Проверка того, что сжатие действительно работает

Перед тем как отлаживать конфигурацию, убедитесь в том, что проблема:

# Check headers only
curl -sI -H "Accept-Encoding: gzip, br, zstd" https://example.com/api/data   | grep -i "content-encoding\|vary"

# Compare compressed vs uncompressed size
curl -so /dev/null -w "uncompressed: %{size_download} bytes
" https://example.com/api/data
curl -so /dev/null --compressed -w "compressed:   %{size_download} bytes
" https://example.com/api/data

--compressed отправляется Accept-Encoding: deflate, gzip, br, zstd автоматически и декодируется. Если оба значения совпадают, сжатие не включается. Если вы хотите проверить все заголовки ответа и то, что они означают в контексте, то Анализатор HTTP-заголовков добавит аннотации, включая Vary, Content-Encodingи директивы cache-control.

gzip против Brotli против Zstd

Три алгоритма практически значимы для HTTP сегодня. Ниже приведены данные из официальных тестов Zstd на корпусе Silesia — стандартном наборе смешанных реальных файлов (HTML, исходный код, PDF, базы данных), протестированных на процессоре Core i7-9700K. Платформы с чистым JSON или простым текстом обычно сжимаются лучше.

АлгоритмУровеньОтношениеКомпрессРаспаковать
gzip1 (быстрый)2.74x69 МБ/с380 МБ/с
gzip6 (по умолчанию)2.97x29.9 МБ/с360 МБ/с
gzip9 (максимум)3.10x18 МБ/с360 МБ/с
Brotli43.18x104 МБ/с440 МБ/с
Brotli11 (максимум)3.74x0.4 МБ/с440 МБ/с
Zstd1 (быстрый)2.88x430 МБ/с1380 МБ/с
Zstd3 (по умолчанию)3.01x320 МБ/с1350 МБ/с
Zstd19 (максимум)3.40x17.5 МБ/с1380 МБ/с

gzip является базовым. Уровень 6 — правильный выбор для мгновенного сжатия — затраты на 651 ТП7Т больше CPU при переходе от уровня 6 к уровню 9 дают прирост в 41 ТП7Т, что не оправдано для динамических ответов. Для статических файлов, предварительно сжатых, это другое вычисление.

Brotli на самом деле превосходит gzip при сопоставимых затратах CPU на уровнях 4–6 и декодируется примерно на 201 ТП7Т быстрее. Причина: Brotli имеет статический словарь, настроенный под веб-контент — HTML-сущности, имена полей HTTP, ключевые слова JavaScript. Он даёт лучшие соотношения по сравнению с универсальным компрессором на одном и том же материале. Уровень 11 применим только для предварительно сжатых статических ресурсов; при скорости сжатия 0,4 МБ/с вы сжимаете около 25 МБ в минуту. Это этап сборки, а не обработки запроса.

Zstd является сценарием скорости. Уровень по умолчанию (3) даёт соотношение, равное gzip, но сжимает в 10 раз быстрее и декодирует почти в 4 раза быстрее. Основное ограничение — поддержка браузеров: Chrome 118+ (октябрь 2023), Firefox 126+ (май 2024), Safari 18+ (конец 2024). Ещё не достаточно универсально, чтобы использовать как единственный алгоритм, но если ваш сервер правильно переговаривает, добавление Zstd требует лишь нескольких строк конфигурации и помогает клиентам, которые его объявляют. Уровень Zstd 19 приближается к соотношению Brotli-11 без катастрофической потери скорости сжатия, что делает его более применимым для мгновенного сжатия при высоких требованиях к сжатию.

Поддержка браузеров и клиентов

АлгоритмChromeFirefoxSafariEdgeNode.js
gzipВсеВсеВсеВсеВстроенный (zlib)
deflateВсеВсеВсеВсеВстроенный (zlib)
Brotli (br)51+44+11+15+v10.16+
Zstd118+126+18+118+v21+

Одно важное особенность: br и zstd появляются только в Accept-Encoding при HTTPS-соединениях. Браузеры намеренно не объявляют их при обычных HTTP-соединениях — это защита от атак MITM, которые могут вставить заголовки сжатия. Если вы тестируете на http://localhost и спрашиваете, почему вы видите только gzip, deflate, это именно так. Тестируйте через HTTPS или используйте curl напрямую (curl не применяет это ограничение).

Четыре настройки, которые молча разрушают его

1. отсутствует gzip_proxied (nginx как обратный прокси)

модуль nginx gzip сжимает ответы, которые он сам генерирует. Для прокси-запросов (приложение upstream до nginx до клиента) вам нужно gzip_proxied — иначе nginx сжимает только ответы от своих собственных обработчиков, а не от proxy_pass upstreams.

# This is NOT enough when nginx is a reverse proxy:
gzip on;
gzip_types text/plain application/json application/javascript text/css;

# You need this too:
gzip_proxied any;

Большинство настроек nginx — это обратные прокси. Большинство руководств не упоминают gzip_proxied. Эти два факта объясняют много молча несжатых ответов.

2. MIME-тип не указан в gzip_types

по умолчанию nginx gzip_types является text/html только. JSON, CSS, JavaScript, SVG — все несжаты, если не указано явно:

gzip_types
  text/plain
  text/css
  text/xml
  text/javascript
  application/json
  application/javascript
  application/xml
  application/rss+xml
  image/svg+xml;

nginx проверяет базовый MIME-тип, поэтому application/json охватывает application/json; charset=utf-8. Не нужно указывать варианты кодировки отдельно.

3. Промежуточный прокси удаляет Accept-Encoding

AWS ALB, неправильно настроенные Cloudflare Workers и некоторые настройки API-гейтвей удаляют или изменяют Accept-Encoding до того, как он достигнет вашего источника. Сервер никогда не видит этот заголовок, и по умолчанию не включает сжатие, и все, что идёт далее, думают, что функция сломана, когда на самом деле проблема — в промежуточном слое. В цепочке не появляется никакой ошибки.

Отладка путём сравнения ответа источника и ответа CDN:

# Via CDN/proxy
curl -sI -H "Accept-Encoding: gzip, br" https://example.com/api/data

# Direct to origin (bypassing CDN via --resolve or direct IP)
curl -sI -H "Accept-Encoding: gzip, br" --resolve "example.com:443:ORIGIN_IP" https://example.com/api/data

Если источник возвращает Content-Encoding: gzip прямым образом, а ответ CDN не имеет Content-Encoding, то CDN удаляет что-то — скорее всего удаляет Accept-Encoding на входе, поэтому источник никогда не сжимает вовсе.

4. Приложение upstream сжимает ответ, затем nginx пытается сжать снова

Если ваше приложение на Node.js/Go/Python сжимает тело ответа и устанавливает Content-Encoding: gzip, nginx должен пропустить двойное сжатие — но это зависит от времени заголовков. Если upstream отправляет заголовок в процессе передачи или если обнаружение nginx соревнуется, вы можете получить двойно сжатый мусор, который клиенты не могут декодировать.

Чистое решение: пусть nginx будет отвечать за всё сжатие. Удалите компрессионные модули из вашего приложения (например, модуль express’s compression , Go’s gzip.Handlerи т.д.), возвращайте чистые ответы и пусть nginx сжимает на краю. Тот же уровень производительности, без риска двойного сжатия.

Работающие конфигурации

nginx

gzip on;
gzip_vary on;         # adds Vary: Accept-Encoding automatically
gzip_proxied any;     # compress responses from proxied upstreams
gzip_comp_level 6;
gzip_min_length 256;  # skip tiny responses where overhead isn't worth it
gzip_types
  text/plain
  text/css
  text/xml
  text/javascript
  application/json
  application/javascript
  application/xml
  application/rss+xml
  image/svg+xml;

# Brotli requires the ngx_brotli module
# https://github.com/google/ngx_brotli
brotli on;
brotli_comp_level 4;
brotli_static on;     # serve pre-compressed .br files when they exist
brotli_types
  text/plain
  text/css
  application/json
  application/javascript
  image/svg+xml;

Apache

LoadModule deflate_module modules/mod_deflate.so
AddOutputFilterByType DEFLATE text/html text/plain text/css   application/json application/javascript image/svg+xml
Header append Vary Accept-Encoding

# mod_brotli requires Apache 2.4.26+
LoadModule brotli_module modules/mod_brotli.so
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css   application/json application/javascript image/svg+xml

Caddy

Caddy включает gzip и Brotli по умолчанию. Чтобы явно добавить Zstd:

example.com {
  encode gzip zstd br
  reverse_proxy localhost:3000
}

Нет списков MIME-типов, нет gzip_proxied особенных случаев, корректная Vary обработка встроена. Честный ответ на вопрос «какой сервер имеет наименьшую поверхность ошибок, связанных с сжатием» — это Caddy.

Тестирование сжатия на собственных пакетах

Данные по корпусу Silesia показывают относительную производительность, но ваш конкретный пакет важнее. Повторяющийся ответ API с постоянными именами полей сжимается иначе, чем минифицированный JavaScript или смешанный HTML. Эти инструменты позволяют тестировать конкретные пакеты прямо в браузере без запуска локального сервера сжатия:

Полезно при принятии решения о предварительном сжатии статических ресурсов на уровне Brotli-11 или о том, чтобы просто позволить nginx сжимать на лету с gzip. Вставьте ваш реальный ответ, сравните соотношения и сделайте выбор на основе реальных данных.

Подводя итог

Если ответы не сжимаются и curl -sI подтверждает отсутствие Content-Encoding, то решение почти наверняка одно из четырёх вышеуказанных настроек — наиболее вероятно gzip_proxied any; для nginx или CDN, который удаляет ваш Accept-Encoding заголовок. Проверьте источник напрямую, прежде чем обвинять конфигурацию сервера.

Что касается выбора алгоритма: gzip-6 подходит для динамических API-ответов и несёт почти нулевой риск настройки. Добавьте Brotli для статических ресурсов — предварительно сжимайте на уровне 11 в процессе сборки, подавайте с brotli_static on, и пусть nginx будет падать на gzip для клиентов, которые не объявляют br. Zstd стоит добавить сейчас; стоимость настройки минимальна, и его влияние на браузеры растёт. Предложение всех трёх алгоритмов с правильной Vary: Accept-Encoding обработкой — правильная позиция для всего нового.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?