Gzip сжимает веб-страницы более четверти века, и за это время включение уровня 6 стало почти рефлексом: одну директиву прописали, проверили вес ответа и забыли. Формат Brotli прижимает текстовые ресурсы плотнее на 15-25%, поддерживается всеми актуальными браузерами, но в nginx попадает только после сборки с внешним модулем, поэтому вокруг него до сих пор сохраняется лишняя осторожность. Цифры здесь вполне материальные: на медленной мобильной сети экономия двух десятков килобайт оборачивается двумястами миллисекундами до первой крупной отрисовки, а это уже территория поведенческих факторов и позиций в поисковой выдаче. Ниже разобрано, откуда Brotli берёт преимущество над Gzip, сколько байтов он экономит на HTML, CSS и JavaScript и как собрать конфигурацию nginx, в которой оба алгоритма работают одновременно и не дублируют затраты друг друга.

Алгоритм Brotli опирается на встроенный словарь и широкое окно поиска

По внутренней механике Brotli близок к Gzip: компрессор ищет повторяющиеся фрагменты по схеме LZ77 и кодирует найденные совпадения кодами Хаффмана. Преимущество набирается из трёх деталей. Первая, встроенный словарь объёмом около 120 килобайт, хранит примерно 13,5 тысячи самых частых фрагментов веб-текстов: слова нескольких языков, HTML-теги, CSS-свойства, ключевые слова JavaScript. Когда страница содержит последовательность, совпадающую со словарной записью, в архив попадает только короткая ссылка на номер словаря. Для коротких документов это работает как бесплатная предыстория, которой у Gzip нет: файл HTML в 5 килобайт после Brotli весит почти вдвое меньше, чем после Gzip, именно за счёт готовых словарных ссылок.

Вторая деталь, окно поиска. Gzip помнит повторы только в пределах 32 килобайт уже пройденных байтов, Brotli допускает окно до 16 мегабайт. Для крупного бандла JavaScript это значит, что фрагмент кода, повторившийся через 300 килобайт после первого вхождения, всё равно будет закодирован одной ссылкой. Третья деталь, контекстная модель: алгоритм уточняет стоимость каждого символа в зависимости от предыдущих байтов, что особенно помогает разметке и стилям. И наконец шкала качества: у Brotli двенадцать ступеней от 0 до 11 против девяти у Gzip, и верхние ступени дожимают заметно плотнее, пусть и ценой процессорного времени. История у формата короткая: Google представил его в 2013 году внутри шрифтового формата WOFF2, а с 2015 года Brotli применяется как общий способ кодирования ответов сервера.

Скорость распаковки у обоих форматов сопоставима, браузер расшифровывает поток Brotli примерно так же быстро, как Gzip, а сам словарь уже зашит в браузер и по сети не передаётся. Всю цену компактности платит сервер, поэтому настройка сводится к выбору уровня под конкретный сценарий: быстрый для ответов, которые генерируются на ходу, и максимальный для файлов, сжимаемых один раз на этапе сборки. Сколько это в байтах на реальных ресурсах, показывают замеры.

Экономия на HTML, CSS и JavaScript доходит до 25 процентов против Gzip шестого уровня

Замеры на типичных текстовых файлах дают устойчивую картину. Brotli уровней 4 и 5 при сжатии на лету отжимает контент на 15-25% сильнее, чем Gzip уровня 6, при сопоставимом расходе процессора. Предварительное сжатие уровнем 11 добавляет ещё несколько процентных пунктов, и на необработанной разметке выигрыш над Gzip 6 доходит до четверти и выше. Нюанс, о котором часто забывают: минифицированный JavaScript сжимается плотнее в абсолютных числах, но относительный отрыв Brotli на нём скромнее, около 10-15%, потому что минификация убирает часть закономерностей, на которые рассчитан словарь. Зато HTML Brotli любит: разметка насыщена тегами и типовыми атрибутами, и выигрыш в 20-25% на ней встречается регулярно.

Типичные значения веса для минифицированных файлов выглядят так. Таблица стилей фреймворка на 120 килобайт после Gzip 6 весит 22 килобайта, после Brotli 5 остаётся 18, после Brotli 11 выходит 17. Бандл JavaScript на 300 килобайт превращается в 95 килобайт под Gzip, в 80 под Brotli 5 и в 74 под Brotli 11. Страница HTML на 100 килобайт занимает 25 килобайт под Gzip, около 20 под Brotli 5 и 19 под Brotli 11. Файл SVG-иконок на 60 килобайт сжимается примерно до 15 килобайт под Gzip и до 12 под Brotli, ведь внутри обычный текст разметки. Ответ API в JSON на 200 килобайт падает до 40 под Gzip и примерно до 33 под Brotli 4.

Что даёт владельцу сайта разница в паре десятков килобайт? На языке сетевых задержек лендинг, который с Gzip весил 142 килобайта, с Brotli 5 занимает 118 килобайт, а с предсжатыми файлами уровня 11 только 110. При соединении 10 Мбит/с экономия от 24 до 32 килобайт на загрузку означает 20-26 миллисекунд, при 1 Мбит/с уже 200-260. Для метрики Largest Contentful Paint, которая участвует в оценке качества страниц при ранжировании, четверть секунды на слабом канале часто переводит страницу из зоны "требует улучшения" в зелёную. На масштабе это ощутимо и в трафике: при 10 миллионах просмотров в месяц лишние 24 килобайта с Gzip обходятся примерно в 240 гигабайт исходящего трафика, которого могло бы не быть.

Динамическое сжатие держат на уровне 5 ради скорости ответа сервера

Сжатие на лету выполняется для каждого ответа заново, поэтому цена уровня сразу отражается и на времени ответа, и на нагрузке. Brotli уровней 4 и 5 по скорости близок к Gzip 6: одно ядро обрабатывает порядка 20-40 мегабайт в секунду, страница HTML на 50 килобайт сжимается за 2-4 миллисекунды, ответ API на 20 килобайт укладывается в одну миллисекунду. Уровень 11 на тех же объёмах работает в десятки и сотни раз дольше, счёт идёт на десятки и сотни миллисекунд. Арифметика показывает границу применимости: при потоке 200 запросов в секунду и цене 3 миллисекунды на ответ уходит около 0,6 ядра только на сжатие, что терпимо. На уровне 11 тот же поток потребовал бы десятков ядер, что лишено смысла даже на большом сервере.

Практический вывод двойной. Для динамических ответов HTML, JSON и генерируемой на ходу статики уровень задают в диапазоне 4-5, этого хватает, чтобы обойти Gzip 6 на 15-20% и не подорожать по времени ответа. Файлы, не меняющиеся между релизами, готовят заранее с максимальным качеством, потому что сборку никто не ограничивает секундами ожидания клиента. У Gzip разумный потолок тот же по духу: уровень 6, поскольку девятка выигрывает у шестёрки 1-2% и работает втрое медленнее.

Память под окно тоже учитывают. Директива brotli_window по умолчанию равна 512k, и буфер такого объёма выделяется на каждое сжимаемое соединение. Типичным страницам этого хватает с запасом, расширение до 16m оправдано только для больших бандлов, а платит за него оперативная память пропорционально числу одновременных сжатий.

Минификация подготавливает файлы к сжатию и умножает экономию

Сжатие работает тем лучше, чем однороднее подготовлен файл, поэтому перед архивированием исходники прогоняют через минификаторы: terser для JavaScript, cssnano или lightningcss для стилей, html-minifier-terser для разметки. Минификация снимает комментарии, пробелы и переносы, сокращает локальные переменные и убирает необязательные кавычки, снижая вес исходника на 20-40%. Сжатие затем убирает ещё 65-75% от оставшегося, и эффекты перемножаются.

Числовой пример делает картину наглядной. Исходный бандл JavaScript на 480 килобайт после terser занимает 300 килобайт. Gzip 6 сжимает его до 95 килобайт, Brotli 5 до 80, Brotli 11 до 74. Тот же файл без минификации Gzip дожал бы лишь до 135 килобайт: комментарии и длинные имена переменных архивируются легко, но часть объёма всё равно оставляют. Разница между ленивой схемой "сырой файл плюс Gzip" и схемой "минификация плюс Brotli 11" составляет 61 килобайт на одном ресурсе, а сам файл уменьшается в 6,5 раза относительно исходника.

Сборочная линия собирает эти шаги в один проход: минификация, предсжатие уровнем 11 в файлы с суффиксом .br и параллельная копия для .gz. Плагины вроде vite-plugin-compression или brotli-webpack-plugin делают это автоматически при каждой публикации. Тяжёлые операции выполняются на машине сборки один раз за релиз, а боевому серверу остаётся отдавать готовые байты.

Подключение модуля ngx_brotli к nginx и базовый набор директив

В штатной поставке nginx поддержки Brotli нет, модуль ngx_brotli подключают отдельно как динамический. Исходники модуля размещают в /opt/ngx_brotli, затем пересобирают nginx той же версии, что установлена на сервере, иначе загрузка завершится ошибкой из-за несовпадения сигнатуры версии. Флаг --with-compat снимает часть ограничений совместимости. Команды для сборки выглядят так:

cd /usr/src/nginx
./configure --with-compat --add-dynamic-module=/opt/ngx_brotli
make modules
cp objs/ngx_http_brotli_filter_module.so /etc/nginx/modules/
cp objs/ngx_http_brotli_static_module.so /etc/nginx/modules/

В начало nginx.conf добавляют две строки load_module, а в блок http попадает сам набор настроек. Рабочая конфигурация с одновременным Brotli и Gzip выглядит так:

load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

http {
    brotli on;
    brotli_comp_level 5;
    brotli_types text/css application/javascript application/json
                image/svg+xml text/plain application/xml
                font/ttf font/otf;
    brotli_static on;
    brotli_window 512k;

    gzip on;
    gzip_comp_level 6;
    gzip_static on;
    gzip_vary on;
    gzip_min_length 256;
    gzip_types text/css application/javascript application/json
               image/svg+xml text/plain application/xml;
}

Назначение директив коротко: brotli включает фильтр сжатия в текущем контексте, brotli_comp_level задаёт уровень от 0 до 11 со значением по умолчанию 6, brotli_types перечисляет MIME-типы поверх всегда сжимаемого text/html, brotli_static разрешает отдачу готовых файлов с суффиксом .br, brotli_window ограничивает буфер поиска повторов. Пара gzip-директив устроена зеркально, а gzip_vary on добавляет заголовок Vary: Accept-Encoding к ответам, без которого промежуточный кеш может отдать сжатую версию браузеру без поддержки. Значение always у brotli_static раздаёт файлы .br безусловно и годится разве что для закрытых интеграций, в обычной работе оставляют on.

Выбор формата при таком наборе происходит без скриптов и условий: если браузер прислал Accept-Encoding с пометкой br, nginx отдаёт Brotli, если только gzip, достаётся Gzip, при пустом Accept-Encoding ответ уходит несжатым. Один и тот же URI получает разные представления, конфликтов между модулями не возникает. Директивы разрешено уточнять в блоках server и location, поэтому раздачу ответов проверки работоспособности или уже сжатых файлов можно отключать точечно.

Предварительное сжатие уровня 11 отдаётся без затрат процессора

Для статики, которая меняется только между релизами, динамическое сжатие избыточно. Файлы готовят заранее утилитой brotli из одноимённого пакета, и рядом с каждым исходником ложится сжатая копия с суффиксом .br:

apt install brotli
brotli -q 11 -k dist/assets/main.css dist/assets/app.js
find dist/assets -type f \( -name "*.css" -o -name "*.js" \) -exec brotli -q 11 -k {} \;

Уровень 11 на файле в 300 килобайт занимает от половины секунды до секунды, сборка из двадцати файлов ждёт около минуты, но происходит это на машине публикации единожды за релиз. Для ускорения каталог сжимают параллельно, передавая список файлов в xargs с опцией -P 4 и распределяя работу по ядрам. В бою nginx с включённым brotli_static просто читает готовые байты с диска, не тратя ни миллисекунды процессора, поэтому время до первого байта не отличается от отдачи несжатого файла. Рядом логично держать и .gz копии через gzip_static, поскольку клиенты без Brotli продолжат получать Gzip, а лишнее место на диске дешевле лишних ядер на проде.

Границы применимости те же, что и всегда. Уже сжатые форматы вроде png, jpg, webp и mp4 повторному сжатию не подлежат, процессор на них уходит впустую, поэтому в brotli_types они не попадают. Шрифты woff2 трогать бессмысленно вдвойне: формат внутри уже использует Brotli. Файлы легче одного килобайта архивировать тоже незачем, служебные байты съедят экономию. Типичная ошибка конфигурации: если приложение на бэкенде уже сжимает ответы собственным Gzip, nginx не сможет переупаковать их в Brotli и отдаст чужой сжатый ответ как есть. Сжатием должен заниматься ровно один слой, обычно это пограничный nginx, а у бэкенда компрессию выключают.

Браузеры запрашивают Brotli только по HTTPS и держат Gzip как запасной формат

Поддержка формата в браузерах полная: Chrome, Firefox, Safari и Edge понимают br с 2016-2017 годов. У поддержки есть особенность, о которой вспоминают обычно при отладке: заголовок Accept-Encoding с пометкой br клиенты отправляют только поверх TLS. Историческая причина в старых сетевых посредниках, искажавших заголовки на чистом HTTP, поэтому разработчики браузеров ограничили Brotli защищёнными соединениями. Internet Explorer 11 не знает br вовсе. Отсюда следствие: Gzip выключать нельзя, на HTTP-запросах и старых клиентах он остаётся единственным рабочим механизмом, и пара gzip on рядом с brotli on в конфигурации не перестраховка, а норма.

Проверить настроенную связку можно за пару минут, последовательность шагов выглядит так:
1. Команда curl -sI -H "Accept-Encoding: br" https://site.ru/ | grep -i content-encoding должна вернуть строку Content-Encoding: br;
2. Повтор запроса с заголовком Accept-Encoding: gzip показывает запасной механизм, в ответе ожидается Content-Encoding: gzip;
3. Запрос curl -sI https://site.ru/ | grep -i vary должен найти заголовок Vary: Accept-Encoding, он не даст кешу смешать версии для разных браузеров;
4. Проверка статики командой curl -sI -H "Accept-Encoding: br" https://site.ru/assets/app.js | grep -i content-encoding подтверждает, что сжимаются скрипты и стили, а не только HTML;
5. Тот же запрос к странице без шифрования вернёт gzip или пустую строку, поскольку br браузеры запрашивают только по HTTPS.

После проверки остаётся наблюдать за двумя числами в мониторинге: долей ответов с Content-Encoding: br и метрикой Largest Contentful Paint на 75-м перцентиле. Первое покажет, какая часть трафика переехала на новый формат, второе подтвердит, что сэкономленные килобайты превратились в миллисекунды для самых медленных посетителей. Gzip не исчезает из конфигурации, он занимает место страховки, которая достаётся немногим, но срабатывает безотказно в любом сценарии.