Подсказка про включение сжатия текста в автоматических аудитах знакома каждому. Разработчик открывает конфигурацию, добавляет gzip для всего, подсказка гаснет, оценка растёт, и тема закрывается до следующего аудита. Проблема просыпается позже, в час пик, когда процессор веб-сервера занят перекачиванием уже сжатых картинок, а посетители со свежими браузерами получают худшее сжатие из возможного. Рабочих кодировок в HTTP три, gzip, brotli и zstd, плюс свежие словарные надстройки над последними двумя, и разница между ними измеряется и в размере, и в миллисекундах процессора, и в деньгах за оборудование. Выбор между ними это не галочка в чек-листе, а инженерное решение под конкретный тип содержимого.
Переговоры о кодировке которые решают всё остальное
Механика проста до примитивности. Браузер в каждом запросе перечисляет поддержанные кодировки в заголовке Accept-Encoding, сервер выбирает одну, сжимает ответ и подписывает результат заголовком Content-Encoding. Клиент без поддержки brotli получит gzip, клиент без поддержки gzip получит несжатое содержимое, и вся эта торговля работает молча и автоматически. Убедиться, что сервер действительно отдаёт ожидаемое, можно одной командой:
curl -sI -H 'Accept-Encoding: gzip, br, zstd' https://site.example/assets/app.js | grep -i content-encoding
Справедливости ради, весь разговор касается только тела ответа. Заголовки в HTTP/2 и HTTP/3 сжимаются самим протоколом отдельным механизмом, поэтому килобайты повторяющихся заголовков на каждой паре запрос-ответ клиенту и серверу лишний раз не гоняются.
Коварство прячется в кэшах на промежуточных серверах. Если общий кэш положил к себе brotli-версию ответа и не записал заголовок Vary со значением Accept-Encoding, он рано или поздно отдаст её браузеру, который brotli не просил, и тот сломается на распаковке. Поэтому Vary обязателен для любого сжатого ответа, проходящего через общие кэши, и проверять его нужно раньше, чем хвалиться сэкономленными килобайтами.
Проверенный gzip и его рабочий диапазон уровней
Gzip описан стандартами ещё в девяностые и понимается буквально всеми клиентами, включая старые встроенные браузеры бытовой техники. Внутри это словарное сжатие LZ77 в паре с кодами Хаффмана, и у алгоритма девять уровней силы. Разница между соседними уровнями в размере мала, а в процессорном времени заметна, поэтому рабочая зона для динамических ответов лежит посередине, на четвёртом-шестом уровнях: почти максимальное ужимание за долю миллисекунд. Девятый уровень против шестого экономит считаные проценты размера, зато умножает процессорное время, и под боевой нагрузкой эта разница превращается в очередь процессов. В зрелых проектах уровень иногда делают подвижным: при росте очереди сервер переходит на быстрый уровень, при простое возвращается на экономный. Процессор важнее килобайт в тот момент, когда очередь уже пошла.
Арифметика для чувства масштаба. Страница HTML в сто килобайт на среднем уровне сжимается примерно до двадцати пяти-тридцати килобайт за единицы миллисекунд на типичном серверном ядре. Для динамического ответа это честная цена, тем более что повторяющиеся элементы разметки сжимаются отлично. А вот для каждого мелкого ответа API в пару килобайт сжатие уже спорно: служебные заголовки и вызовы съедают выигрыш, а процессор платит за каждый запрос. Третья кодировка из истории, deflate, осталась в стандарте ради совместимости: ранние реализации путались между сырым потоком и обёрткой zlib, эта неразбериха отпугнула всех, и в живых конфигурациях deflate сегодня почти не встречается.
Есть у сжатия на лету и скрытая сторона: длина сжатого ответа заранее неизвестна, поэтому сервер переключается на потоковую передачу без Content-Length. Часть старых клиентов и кэшей относится к таким ответам осторожно, и это ещё один повод доверять сжатие проверенной реализации веб-сервера или фреймворка, а не самописной обёртке внутри приложения.
Brotli который выжимает максимум из готовых файлов
Brotli описан отдельным стандартом и поддержан всеми современными браузерами с 2016-2017 годов. Главная его особенность, встроенный в формат словарь из тринадцати с половиной тысяч частых слов и фраз: разметка, скрипты и стили насыщены повторяющимися строками, и словарь экономит сотни байт ещё до начала работы алгоритма. Уровней качества одиннадцать, и работать с ними нужно асимметрично. Словарь встроен в сам формат и потому одинаков для всех, подложить в него слова своего домена нельзя, для этого существует отдельная словарная спецификация, о которой ниже.
Верхний уровень сжимает текстовые активы на пятнадцать-двадцать процентов лучше, чем gzip на девятом, но готовит сотню килобайт десятками и сотнями миллисекунд. Для ответа на лету это неприемлемо, для файла на диске незаметно. Отсюда рабочая схема: сборка проекта готовит сжатые копии заранее, на максимальном уровне, а веб-сервер просто подхватывает их по запросу. Строка brotli_static в конфигурации делает ровно это и стоит десятка минут настройки. Ни один запрос не платит процессором за верхний уровень, зато каждый получает лучшее ужимание, доступное в вебе. Побочный бонус, заблаговременная подготовка выносит сжатие из боевого контура: ночью на сборочной машине оно бесплатное, днём серверу достаётся только чтение готового файла.
Типовой результат для сборки фронтенда выглядит так: бандл в пятьсот килобайт превращается в gzip-копию около ста двадцати килобайт и в brotli-копию около ста. Разница в двадцать килобайт умножается на число файлов и на каждого нового посетителя, а для регионов с дорогим мобильным трафиком это уже не абстракция. Подготовить обе копии и позволить серверу выбирать по заголовку клиента стоит меньше усилий, чем один спор о том, заметна ли эта разница.
Zstd который меняет раскладку для динамических ответов
Zstandard описан собственным стандартом и добрался до браузеров недавно: Chromium понимает кодировку zstd со сто двадцать третьей версии, выпущенной в двадцать четвёртом году. Остальные браузерные движки пока держат паузу, поэтому gzip в списке Accept-Encoding остаётся обязательным запасным вариантом ещё годы.
Продаёт zstd скорость. На сопоставимом с gzip уровне качества он сжимает в разы быстрее, а распаковывает ещё стремительнее, приближаясь к гигабайтам в секунду на одном ядре. Размер не уступает gzip-у, а на средних уровнях лучше. Для динамических ответов, которые нельзя приготовить заранее, эта комбинация означает: тот же размер за в разы меньшее процессорное время либо лучший размер при прежнем процессоре. API-сервисы с тысячами мелких JSON-ответов в секунду замечают это на графиках загрузки в первый же день. Для мелких ответов обычно хватает третьего-пятого уровня: дальше кривая размера упирается в потолок, а процессорный счёт растёт линейно. У алгоритма двадцать два уровня плюс быстрые отрицательные, то есть плавная ручка между размером и процессором вместо двух крайностей.
Отдельно про совместимость: сервер выбирает кодировку строго по списку клиента, поэтому переход на zstd не ломает никого. Старые браузеры продолжают получать gzip, свежие Chromium забирают zstd, и миграция раскатывается без единого изменения во фронтенде.
Серверная поддержка пока распространена неровно: свежие версии веб-серверов и сред исполнения добавляют zstd по мере выхода, и перед включением стоит проверить, что конкретная сборка вообще умеет нужную кодировку. Отдельное направление, словарные кодировки dcb и dcz из свежей спецификации Compression Dictionary Transport: стороны договариваются об общем словаре и жмут мелкие повторяющиеся ответы так плотно, как раньше умела только связь между собственными службами одного дата-центра.
Раскладка кодировок по типам содержимого
Ошибку сжатия всего подряд лечит короткое правило разложения по типам:
- текстовые форматы HTML, CSS, JavaScript, SVG, JSON и XML ужимаются в три-пять раз и сжимаются всегда;
- готовую статику готовим заранее brotli на верхнем уровне и отдаём с диска без пересчёта;
- динамические ответы страниц и API жмём на лету gzip на среднем уровне, а там, где поддержано, zstd;
- форматы с внутренним сжатием JPEG, PNG, WebP, AVIF, MP4, WebM и woff2 не пропускаем ни через одну кодировку;
- ответы короче килобайта не сжимаем, служебные заголовки съедают весь выигрыш;
- повторяющиеся мелкие ответы одного вида проверяем на словарные кодировки, они придуманы ровно для этого случая.
Пункт про картинки и шрифты звучит очевидно, но именно он чаще всего нарушен в живых проектах. Повторное сжатие уже сжатого файла не даёт ничего, кроме потраченного процессора: алгоритм не находит повторов, а иногда результат выходит длиннее исходника. Пользователь платит задержкой, администрация платит за процессор, и никто не получает ни байта выгоды.
Другая постоянная путаница связана с типом HTML: в примерах конфигураций он не указан, потому что сжимается по умолчанию, и разработчик, не найдя его в списке, решает, что страницы не сжимаются вовсе. Проверка заголовков curl-ом снимает этот вопрос за секунду и заодно показывает, какие типы в конкретной сборке сжаты, а какие нет.
Ошибки в живых конфигурациях которые не видны на глаз
Первая типовая ошибка, широкий список типов из шаблона: в настройку попадают форматы картинок и шрифтов, и сервер пашет впустую на каждом медиа-запросе. Вторая, верхний уровень brotli на лету: тест на ноутбуке показывает красивый размер, а под нагрузкой процессы выстраиваются в очередь. Третья, потерянный Vary, из-за которого часть браузеров получает чужую кодировку и ломается на распаковке. Четвёртая, забытые заранее приготовленные копии: сервер умеет их отдавать, но сборка их не кладёт, и весь верхний уровень простаивает. Рабочая базовая конфигурация выглядит скромно:
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_static on;
brotli_static on;
Строка brotli_static требует модуля, которого нет в базовой поставке веб-сервера, он устанавливается отдельно, зато после включения отдаёт готовые файлы наилучшего качества. Полезно и сравнить конфигурации под нагрузкой на учебном стенде: короткий прогон генератора запросов с включённым и выключенным сжатием показывает реальную цену процессора для вашего железа, и спор о уровнях заканчивается цифрами. Проверка после правки занимает минуту: тот же curl с заголовком Accept-Encoding, по одному файлу из каждой группы, взгляд на Content-Encoding в ответах.
Числа для собственных файлов снимаются прямо на сборочной машине, и это честнее любых чужих таблиц:
time gzip -6 -c app.js > app.js.gz
time zstd -7 -c app.js > app.js.zst
time brotli -q 11 -c app.js > app.js.br
ls -l app.js app.js.gz app.js.zst app.js.br
Пара прогонов на типовом бандле и на типовой странице покажет и размер, и секунды процессора для вашего контента, после чего выбор уровней перестанет быть лотереей.
Вывод который превращает сжатие в осознанный выбор
Gzip, brotli и zstd не отменяют друг друга, а делят работу. Gzip остаётся универсальным языком для динамики и резервом для старых клиентов. Brotli забирает статику, но только при заготовке файлов заранее, на сборке. Zstd наступает на территорию динамики там, куда дотянулась поддержка, и обещает вырасти вместе со словарными кодировками. Формальное включение сжатия для всего подряд платит процессором за картинки, которые не станут меньше, и недоплачивает браузерам, которые умеют лучше. Час на раскладку по типам, минута на проверку заголовков, и сжатие перестаёт быть галочкой из автоматического аудита, становясь обычной настройкой со своими измеренными цифрами.
И последнее наблюдение из практики. Сжатие почти всегда настраивают один раз и потом забывают на годы, хотя контент проекта меняется: у API появляется больше JSON, у фронтенда тяжелее сборки, в ответах больше кандидатов на словарное сжатие. Раз в полгода стоит возвращаться к этой настройке с тем же curl и той же командой замера, потому что раскладка, верная для прошлого состава проекта, тихо устаревает. Дешёвая проверка по расписанию стоит несравнимо меньше, чем год работы сервера в полсилы.