Gzip, Brotli и Zstd для сжатия веб-ресурсов
Brotli, gzip и zstd для веб-ресурсов: когда использовать каждый формат, как уровень сжатия влияет на размер и как заранее сжимать статические файлы.
Используйте Brotli для статических текстовых ресурсов, gzip — как универсальный резервный вариант, а zstd — для динамических ответов, когда вы контролируете и сервер, и CDN.
Множество бандлов до сих пор отдаётся в gzip только потому, что несколько лет назад так решил сборщик или настройки хостинга по умолчанию, и с тех пор никто не пересматривал ни алгоритм, ни уровень сжатия. Далее разберём, почему статический и динамический контент — это разные задачи, почему уровень сжатия зачастую влияет на результат сильнее, чем название алгоритма, как выполнять предварительное сжатие на этапе сборки, как браузер и сервер договариваются о кодировании и что сжимать не стоит вовсе.
Ключевые выводы
- Brotli на максимальном уровне — правильный выбор для статических JavaScript и CSS, поскольку цена медленного сжатия платится один раз при сборке, а не на каждый запрос.
- Zstd подходит для динамических ответов: Cloudflare измерила, что он сжимает на 42% быстрее Brotli, приближаясь к нему по размеру, со средними коэффициентами 2,56:1 для gzip, 2,86:1 для zstd и 3,08:1 для Brotli.
- Регулятор уровня важен не меньше названия алгоритма: переход на Brotli с сохранением быстрого уровня по умолчанию лишает вас части выигрыша.
- В nginx
gzip_comp_levelпо умолчанию равен 1, аbrotli_comp_levelв ngx_brotli — 6; ни одно из этих значений не является максимальным. - Согласование кодирования происходит целиком в заголовках
Accept-EncodingиContent-Encoding, поэтому смена алгоритма — это настройка сервера или CDN, а не изменение кода приложения.
Brotli, gzip или zstd: рекомендация
Brotli — правильный выбор по умолчанию для статических JavaScript и CSS, потому что сжатие выполняется один раз на этапе сборки, а значит, медленные верхние уровни ничего не стоят во время обработки запроса.
Gzip остаётся в стеке как резервный вариант, поскольку каждый браузер указывает его в Accept-Encoding; он не лучший ни для чего, но это единственный вариант, который никогда не подводит.
Zstd — для динамических ответов, где цена сжатия платится при каждом запросе, поскольку он достигает коэффициента, близкого к Brotli, за малую долю процессорного времени.
Статика против динамики: когда платится цена сжатия?
Выбор между алгоритмами — это выбор момента, когда оплачиваются затраты CPU. Статический бандл сжимается один раз на релиз и отдаётся тысячи раз, поэтому важен только коэффициент, а скорость сжатия не имеет значения. HTML-страница, рендерящаяся на каждый запрос, сжимается при каждом обращении, поэтому скорость сжатия становится вопросом задержки и стоимости серверных ресурсов наравне с коэффициентом.
Анонс поддержки Zstandard от Cloudflare (сентябрь 2024) даёт цифры для динамического случая. В тесте третьего квартала 2024 года, когда трафик тарифа Free на 24 часа переключили с Brotli на zstd (zstd на уровне по умолчанию 3, уровни Brotli и gzip не указаны), zstd сжимал на 42% быстрее Brotli и почти не уступал ему по размеру. Замеренные средние значения составили 2,56:1 для gzip, 2,86:1 для zstd и 3,08:1 для Brotli, и собственный вывод Cloudflare состоял в том, что zstd лучше подходит для динамических ответов, включая HTML.
Итог: Brotli выигрывает там, где единственный критерий — коэффициент сжатия, то есть для любого статического ресурса. Zstd выигрывает там, где сжатие выполняется на каждый запрос.
Уровень сжатия значит не меньше, чем алгоритм
У каждого алгоритма есть регулятор уровня, и уровень часто влияет на результат сильнее, чем смена алгоритма. Brotli в ngx_brotli поддерживает уровни от 0 до 11, gzip в nginx — от 1 до 9, а CLI zstd — от 1 до 19 при значении по умолчанию 3, плюс уровни 20–22 за флагом --ultra.
Насколько сильно регулятор меняет результат, полностью зависит от входных данных, поэтому опубликованные цифры «до и после» для чужого бандла почти ничего не говорят о вашем. Пропустите свои файлы через инструмент сравнения сжатия, который сжимает gzip, Brotli и zstd на любом уровне прямо в браузере, и сравните размеры сами.
Значения по умолчанию объясняют, почему так много сайтов отдаются с настройками «побыстрее». В nginx gzip_comp_level по умолчанию равен 1 — самому быстрому из уровней 1–9. В ngx_brotli brotli_comp_level по умолчанию равен 6 из диапазона 0–11. Ни то, ни другое не является максимумом, и оба разумны для сжатия «на лету» — то есть именно для того контекста, который совершенно не подходит статическим ресурсам. Какой бы уровень вы ни выбрали, платит за него сжимающая сторона. В README самого проекта zstd отмечено, что декодирование выполняется примерно с одинаковой скоростью независимо от уровня, которым был создан файл; то же верно для zlib и lzma.
Итог: задавайте уровень осознанно. Максимум — для всего, что сжимается заранее; средний уровень — для всего, что сжимается на каждый запрос.
Как выполнить предварительное сжатие ресурсов на этапе сборки?
Предварительное сжатие означает, что шаг сборки записывает рядом с каждым ресурсом файлы-«соседи» .br, .gz и при желании .zst, а сервер выбирает подходящий файл, ничего не сжимая во время обработки запроса.
brotli -q 11 app.js # writes app.js.br; source kept by default
gzip -9 -k app.js # writes app.js.gz; -k keeps the source
zstd -19 app.js # writes app.js.zst; source kept by default
CLI brotli по умолчанию сохраняет исходные файлы и принимает -q для качества от 0 до 11. GNU gzip требует -k или --keep, чтобы оставить оригинал на месте.
Для отдачи таких файлов-«соседей» в nginx нужны две директивы:
load_module modules/ngx_http_brotli_static_module.so;
http {
gzip_static on; # serves .gz siblings; module needs --with-http_gzip_static_module
brotli_static on; # serves .br siblings; default off
gzip_vary on; # adds Vary: Accept-Encoding; default off
}
ngx_http_gzip_static_module по умолчанию не собирается. brotli_static предоставляется модулем ngx_brotli. Собственного модуля zstd в nginx нет; сторонний zstd-nginx-module добавляет директиву zstd_static для файлов .zst (по умолчанию выключена), а без него zstd для статических файлов остаётся настройкой на стороне CDN.
CDN пропускают предварительно сжатые файлы. Документация Cloudflare по сжатию сообщает, что заголовок content-encoding: br или gzip, полученный от origin, сохраняется, если браузер посетителя его поддерживает и не включены функции, переписывающие ответ (Rocket Loader, Email Address Obfuscation, Polish и другие); запросы к origin отправляются с accept-encoding: br, gzip, поэтому zstd с origin не пропускается. Поведение Brotli Support у Akamai отдаёт и кэширует сжатый на origin Brotli, возвращает не-Brotli варианты клиентам, которые не принимают br, и само сжатие на edge не выполняет.
Итог: сжимайте на этапе сборки, на максимальном уровне, и настройте сервер или CDN на отдачу файла-«соседа».
Как браузер и сервер договариваются о кодировании?
Браузер перечисляет кодировки, которые умеет декодировать, в Accept-Encoding, сервер или CDN выбирает одну и помечает ответ заголовком Content-Encoding; код приложения в этом обмене не участвует.
GET /app.3f2a1b.js HTTP/1.1
Accept-Encoding: gzip, deflate, br, zstd
HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding
br — токен Brotli в реестре HTTP content-coding, определённый в RFC 7932, раздел 13. Vary: Accept-Encoding указывает разделяемым кэшам не отдавать тело в Brotli клиенту, который запрашивал только gzip; nginx не добавляет этот заголовок, если не задано gzip_vary on.
Чтобы узнать, что сайт отдаёт сегодня, запросите один файл бандла и прочитайте единственный заголовок в ответе:
curl -sI -H 'Accept-Encoding: gzip, br, zstd' https://example.com/app.js | grep -i content-encoding
Та же информация доступна в панели Network инструментов разработчика в разделе заголовков ответа.
Порядок резервных вариантов определяется поддержкой в браузерах. Brotli поддерживается всеми актуальными основными браузерами. caniuse указывает zstd в Chrome и Edge начиная с версии 123, в Firefox — с 126, в Opera — с 109 и в Safari — с 26, причём для десктопного Safari отмечена частичная поддержка. Любому браузеру, который не указывает zstd в Accept-Encoding, просто отдаётся Brotli или gzip. Сценария отказа здесь нет — есть только резервный вариант.
Итог: смена алгоритма — это изменение конфигурации, а цепочка резервных вариантов делает её безопасной.
Что сжимать не нужно?
Повторное сжатие JPEG, MP4 или WOFF2 тратит CPU на обеих сторонах ради практически нулевого выигрыша, поскольку эти форматы уже сжаты внутренне. WOFF2 — самый наглядный случай: RFC 7932, раздел 1.2 фиксирует, что определяемый им формат встроен в WOFF 2.0, то есть файл .woff2 уже является результатом работы Brotli.
Второе исключение — очень маленькие ответы. Ниже нескольких десятков байт накладные расходы кодирования превышают выигрыш, поэтому gzip_min_length в nginx и brotli_min_length в ngx_brotli по умолчанию равны 20 байтам, а Cloudflare сжимает только ответы размером не менее 48 байт для gzip и 50 байт для Brotli и zstd.
Выводы, изложенные здесь, относятся к файлам, которые реально получают браузеры — от нескольких килобайт до нескольких мегабайт. Бенчмарки, где Brotli работает минутами, получены на файлах в сотни мегабайт, которые никогда не передаются через Content-Encoding.
Итог: сжимайте текст (HTML, CSS, JavaScript, JSON, SVG); пропускайте медиа, шрифты и крошечные ответы.
Заключение
Вопрос выбора алгоритма имеет устоявшийся ответ: Brotli на максимальном уровне для всего, что сжимается на этапе сборки, zstd для всего, что сжимается на каждый запрос, gzip как минимальный уровень, понятный любому клиенту. А вот вопрос уровня сжатия большинство стеков вообще никогда не задавало. Выполните проверку через curl на собственном бандле, посмотрите кодировку и оцените уровень по размеру, и если ответ — gzip на быстром уровне по умолчанию, то от предварительного сжатия через Brotli вас отделяют один шаг сборки и две серверные директивы.
Часто задаваемые вопросы
Почему мой сайт всё ещё отдаёт gzip, хотя браузер поддерживает Brotli?
Почти все случаи объясняются тремя причинами. Во-первых, Chrome и Firefox указывают 'br' в Accept-Encoding только по HTTPS, поэтому запросы по обычному HTTP, включая большую часть разработки на localhost, откатываются к gzip. Во-вторых, на сервере не загружен модуль Brotli: nginx требует ngx_brotli, который не входит в базовую сборку. В-третьих, CDN вроде Cloudflare пропускает ответ origin, уже помеченный content-encoding: gzip, вместо перекодирования его в Brotli.
В чём разница между 'deflate' и 'gzip' в Content-Encoding?
Оба используют один и тот же алгоритм DEFLATE из RFC 1951 и различаются только обёрткой. В HTTP 'deflate' означает формат zlib из RFC 1950 (двухбайтовый заголовок, контрольная сумма Adler-32), а 'gzip' — контейнер RFC 1952 с завершающей CRC-32. Ранние серверы и браузеры иногда отдавали «сырой» DEFLATE под именем 'deflate', заставляя клиентов угадывать, поэтому gzip стал надёжным выбором, а deflate редко стоит предлагать.
Поддерживает ли Node.js сжатие Brotli и zstd нативно?
Да. Встроенный модуль node:zlib реализует content-encoding gzip, deflate, br и zstd без сторонних пакетов. Brotli доступен начиная с Node.js 11.7.0 через zlib.brotliCompress и zlib.createBrotliCompress; Zstandard появился в Node.js 23.8.0 с zlib.zstdCompress и zlib.createZstdCompress, а в Node.js 24.6.0 в API zstd добавлена поддержка словарей (https://nodejs.org/en/blog/release/v24.6.0). Middleware для сжатия, выпущенное до этих релизов, может по-прежнему согласовывать только gzip, поэтому проверяйте, какой content-encoding оно отдаёт.