1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Что сжимать и чем

Коротко

Сжатие - самая дешёвая оптимизация на стороне nginx. Замер: 300 КБ текста уезжают как 85 КБ.

  • По умолчанию сжимается только text/html. Ни CSS, ни JS, ни JSON без gzip_types не жмутся вовсе.
  • Уровень 9 против 5: минус 6% байт за втрое большее время. Рабочий диапазон - 4-6.
  • Для неизменяемой статики жать на лету незачем: gzip_static on отдаёт готовый .gz.
  • gzip_vary on обязателен за любым кешем, а brotli и zstd в стандартную сборку не входят.

Если помнишь, что gzip_types по умолчанию покрывает один тип, - листай до «Теперь сам».

Сначала ответь сам

В конфиге написано ровно gzip on;. Что сожмётся из этого списка: HTML-страница, app.js, data.json, photo.jpg?

Замер на стенде, размер ответа при Accept-Encoding: gzip:

$  команда
for f in data.json app.js photo.jpg; do printf '%-12s ' $f; curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' http://shop.local/$f; done
↳  выводfor f in data.json app.js photo.jpg; do ...; done (gzip on, больше ничего)
data.json    120391
app.js        78000
photo.jpg   2000000

Только HTML. gzip_types по умолчанию содержит один тип - text/html, - и это единственное, что жмётся из коробки. Добавляем типы:

↳  выводте же запросы после gzip_types application/json application/javascript
data.json     2204   (было 120391)
app.js         526   (было 78000)

На что это похоже

Вакуумный пакет для вещей. Свитер сжимается в четыре раза, потому что в нём воздух. Кирпич в таком пакете не сжимается вовсе - в нём нечего выдавливать, - а время на упаковку ты потратишь.

Текст - это свитер: в нём полно повторов. JPEG, видео и woff2 - кирпичи, внутри уже сжатые своим алгоритмом.

Механизм: договор через два заголовка

браузер nginx Accept-Encoding: gzip, br, zstd Content-Encoding: gzip Vary: Accept-Encoding Vary говорит кешам на пути: у этого адреса есть несколько версий, выбирать надо по заголовку запроса без него кеш однажды отдаст сжатое тому, кто сжатия не понимает
Сжатие всегда по договорённости: клиент говорит, что понимает, сервер выбирает.

gzip_vary по умолчанию off

Заголовок nginx сам не добавит. Ошибка редкая, но зато крайне неприятная: воспроизводится только через кеширующий прокси и только у части пользователей - они видят бинарный мусор вместо страницы.

Правило

/etc/nginx/nginx.confhttp
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_types text/css text/plain text/xml application/javascript application/json
           application/xml image/svg+xml application/wasm;
Сжимать Не сжимать
HTML, CSS, JS, JSON, XML, SVG JPEG, PNG, WebP, AVIF
шрифты .ttf, .otf, .eot шрифты .woff, .woff2 (сжаты внутри)
.txt, .csv, .map, .wasm видео, аудио, .zip, .gz, .pdf

text/html в список писать не нужно - он сжимается всегда. И смотри на content-type из ответа: application/javascript и text/javascript - разные строки, и если бэкенд отдаёт вторую, а в конфиге первая, сжатия не будет.

Разбор: почему не девятка

gzip_comp_level принимает от 1 до 9, по умолчанию 1. Замер на 300 КБ реального текста, лучшее время из семи прогонов:

уровень размер время
без сжатия 300 000 байт -
1 111 667 (37%) 5,9 мс
5 85 355 (28%) 12,7 мс
9 79 704 (27%) 39,3 мс

Переход с 1 на 5 стоит удвоения времени и даёт минус четверть байт - выгодно. Переход с 5 на 9 стоит втрое большего времени, а размер режет всего с 28% до 27% от оригинала - то есть примерно на 6% относительно уже сжатого (85 355 → 79 704). На лету это не окупается. Отсюда и рабочий диапазон 4-6.

Разбор: как не платить процессором вовсе

/etc/nginx/conf.d/shop.local.confhttp server location /assets/
gzip_static on;

nginx поищет рядом с app.js готовый app.js.gz и отдаст его как есть. Замер: файл, сжатый gzip -9 при сборке, весит 262 байта, и отдаётся он без единого такта на сжатие:

↳  выводcurl -sI -H 'Accept-Encoding: gzip' http://shop.local/app.js
Content-Length: 262
Content-Encoding: gzip

Готовый .gz кладут рядом с файлом при сборке: gzip -9 -k app.js создаёт app.js.gz, не удаляя оригинал (-k). Файл готовится один раз при сборке фронтенда - там можно спокойно жать девятым уровнем, это уже не стоит времени запроса. Так делают для всей неизменяемой статики с хешем в имени.

Что ломается без этого

Сжатие меняет ответ сильнее, чем кажется, и два побочных эффекта всплывают в других главах.

Пропадает Content-Length. Сжатый ответ едет как Transfer-Encoding: chunked - размер заранее неизвестен. Отсюда две вещи, которые ты уже видел: докачка по Range на сжатых типах не работает вовсе (пятый раздел), а ETag становится слабым (W/"...").

Поток задерживается. Сжатие обязано накопить данные, прежде чем их сжать, - именно оно, а не буферизация, копит потоковый ответ (седьмой раздел). На маршруте с событиями gzip off; ставят осознанно.

Зачем это в работе

Brotli на текстовых файлах даёт примерно на 15-20% меньше байт, чем gzip, и поддерживается всеми браузерами больше пяти лет. Zstd жмёт быстрее при похожем результате.

Важная деталь: в стандартной сборке nginx их нет. gzip - штатный модуль, а brotli и zstd - внешние: ngx_brotli собирается как динамический модуль (в популярных дистрибутивах есть готовые пакеты), zstd - только сторонние сборки. Поэтому строка brotli on; в чужом конфиге - не признак свежей версии, а признак того, что у человека собран модуль; на голом nginx:stable такой конфиг просто не запустится.

Порядок внедрения: gzip первым, он бесплатен и работает везде; gzip_static вторым, он снимает нагрузку; brotli третьим, когда готов собирать модуль и поддерживать его при обновлениях.

Теперь сам

На сайте включили gzip on; gzip_comp_level 9; и добавили все типы. Через неделю мониторинг показывает рост $request_time на страницах каталога при неизменном $upstream_response_time. Что произошло и что делать?

Время выросло на сжатии: девятый уровень стоит втрое дороже пятого, а страницы каталога большие. $upstream_response_time не изменился, потому что бэкенд тут ни при чём - тормозит сам nginx. Делать: вернуть уровень 5, а для неизменяемой статики включить gzip_static и жать девяткой при сборке - там время ничего не стоит.

Главное

gzip_types по умолчанию покрывает только text/html: без явного списка не жмётся ни CSS, ни JS, ни JSON. Уровень 4-6 - рабочий диапазон, девятка стоит втрое больше времени ради шести процентов байт. Для неизменяемой статики готовят .gz при сборке и включают gzip_static on. gzip_vary on обязателен за любым кешем, картинки и woff2 жать бессмысленно, а brotli и zstd в стандартную сборку nginx не входят. И помни, что сжатие убирает Content-Length: отсюда сломанный Range и задержка потоковых ответов.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.