Сжатие заголовков: как экономят на том, что повторяется в каждом запросе
Откройте вкладку «Сеть» в браузере на любой более-менее живой странице — и увидите десятки, а то и сотни запросов: картинки, шрифты, скрипты, стили, API-вызовы. У каждого свой набор заголовков, и добрая половина этих заголовков — User-Agent, Accept-Language, куки сессии, Referer — из запроса в запрос почти не меняется. Передавать их текстом заново при каждом обращении — это чистые накладные расходы, которые особенно бьют по производительности именно там, где запросов много, а полезная нагрузка мала. HPACK и QPACK решают эту задачу не сжатием текста «на лету», а более хитрым трюком: обе стороны запоминают, что уже видели, и дальше обмениваются короткими ссылками на память вместо повторения одного и того же текста.
Содержание
- Заголовки, которые почти не меняются от запроса к запросу
- HTTP/1.1: цена повторения одного и того же текста
- HPACK: статическая и динамическая таблица вместо текста
- QPACK: тот же принцип, адаптированный под потерю пакетов в QUIC
- Почему выигрыш особенно заметен на страницах с десятками ресурсов
- Что это значит для настройки вашего сервера
Заголовки, которые почти не меняются от запроса к запросу
Возьмите типичную сессию браузера на одном сайте: клиент один и тот же на протяжении десятков запросов подряд, и значит:
User-Agentне меняется вообще — это одна и та же строка на весь визит, а часто и на много визитов подряд.Accept,Accept-Language,Accept-Encoding— фиксированный набор значений, которые браузер выставляет одинаково для всех запросов к одному origin.Cookie— сессионный идентификатор и пара флагов, которые не меняются, пока жива сессия, и передаются буквально в каждом запросе к домену.Referer— меняется реже, чем кажется: пока пользователь листает один раздел сайта, значение часто одно и то же или меняется предсказуемо.- Заголовки ответа со стороны сервера —
Server,Content-Typeдля однотипных ресурсов,Cache-Controlс одинаковой политикой для целого класса файлов — тоже повторяются пачками.
Если у страницы полсотни подзапросов (стили, шрифты, иконки, аналитика, несколько API-вызовов), то полсотни раз подряд браузер и сервер обмениваются практически идентичным набором пар «имя заголовка — значение». Задача сжатия заголовков — избавиться именно от этого повтора, а не «сжать текст вообще», как делает, скажем, gzip для тела ответа.
HTTP/1.1: цена повторения одного и того же текста
В HTTP/1.1 заголовки — это обычный текст: имя, двоеточие, значение, перевод строки, и так для каждого поля. Никакой общей памяти между запросами протокол не предусматривает: каждый набор заголовков самодостаточен и не зависит от того, что передавалось минуту назад в том же TCP-соединении. Для запроса с тяжёлым телом — скачиваете видео на сотни мегабайт — несколько сотен байт заголовков не имеют значения, это доли процента. Но для запроса к иконке в 400 байт или к API-эндпоинту, отдающему 200 байт JSON, заголовки размером в полкилобайта — уже не накладные расходы, а основная часть трафика запроса.
Проблему отчасти маскировало то, что HTTP/1.1 держит соединение открытым (keep-alive) и переиспользует TCP-сессию — так что хотя бы не платили за новое рукопожатие на каждый файл (о том, почему это соединение всё равно может незаметно обрываться — см. про keep-alive и молчаливые обрывы). Но сами заголовки внутри keep-alive-соединения всё равно передавались текстом заново на каждый запрос — протокол не умел сказать «то же самое, что в прошлый раз».
Когда в HTTP/2 добавили мультиплексирование — десятки запросов идут параллельно в одном TCP-соединении вместо очереди друг за другом, — проблема повторяющихся заголовков стала заметнее количественно: параллельных запросов в единицу времени больше, и текст заголовков дублируется чаще. Отсюда и появился HPACK как обязательная часть спецификации HTTP/2, а не опциональная надстройка.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверHPACK: статическая и динамическая таблица вместо текста
Идея HPACK простая на словах и аккуратная в деталях: вместо передачи пары «имя: значение» текстом каждая сторона хранит таблицу уже известных пар и передаёт только номер записи в этой таблице. Таблиц две.
Статическая таблица — фиксированный список из 61 самой частой пары, зашитый прямо в спецификацию HTTP/2: :method: GET, :scheme: https, accept-encoding: gzip, deflate, распространённые коды статуса. Она одинакова для всех клиентов и серверов в мире — обе стороны просто знают её наизусть. Если нужная пара есть в статической таблице, в поток летит один байт-индекс вместо текста.
Динамическая таблица — собственная память конкретного соединения. Когда клиент впервые отправляет заголовок, которого нет в статической таблице (например, конкретное значение Cookie или кастомный заголовок вашего API), это значение передаётся текстом — но с пометкой «добавь в динамическую таблицу». Сервер добавляет запись к себе, и оба конца теперь знают: запись номер такой-то — это вот эта пара. В следующем запросе того же соединения клиент отправляет уже не текст, а индекс.
Здесь есть тонкость: динамическая таблица — это состояние всего соединения, а не одного запроса или одного потока внутри мультиплексированного HTTP/2-соединения. Она размером в несколько килобайт (обычно 4 КБ по умолчанию, но стороны могут договориться об ином размере через SETTINGS_HEADER_TABLE_SIZE) и работает как FIFO: когда таблица заполняется, старые записи вытесняются под новые. Эффективность динамической таблицы падает, если заголовки в запросах слишком разнообразны — например, если в Cookie или кастомном заголовке передаётся что-то уникальное на каждый запрос: тогда таблица не успевает накопить полезные повторы, и HPACK ведёт себя почти как обычная передача текстом.
Для значений, которые всё же передаются текстом (первое появление в таблице или то, что туда класть не стоит — например, значения с высокой энтропией), HPACK дополнительно применяет хаффмановское кодирование: символы, встречающиеся в HTTP-заголовках чаще (латиница нижнего регистра, дефисы, цифры), кодируются меньшим числом бит, чем редкие. Это работает независимо от индексации и экономит уже на самом первом упоминании значения, до того как оно попадёт в таблицу.
Итоговая картина для типичной сессии: первый запрос к домену передаёт большинство заголовков текстом (с хаффман-кодированием) и параллельно наполняет динамическую таблицу; каждый следующий запрос по тому же соединению для тех же заголовков передаёт уже не текст, а короткие индексы.
QPACK: тот же принцип, адаптированный под потерю пакетов в QUIC
Когда дошло до HTTP/3, повторно взять HPACK как есть не вышло — не из-за самой идеи индексирования, а из-за транспорта. HTTP/2 работает поверх TCP, где пакеты гарантированно приходят по порядку: если сервер сказал «добавь запись в таблицу», а следом «используй индекс на эту запись», получатель прочитает эти два события строго в том порядке, в каком они были отправлены. QUIC, на котором работает HTTP/3, поверх UDP и намеренно проектировался так, чтобы разные потоки внутри одного соединения не блокировали друг друга при потере пакета одного из них. Прямое применение HPACK поверх QUIC воссоздало бы ту самую проблему блокировки головой очереди, ради ухода от которой QUIC и придумывался: индекс на новую запись таблицы мог бы прийти раньше, чем сама вставка, если пакет со вставкой потерялся и ждёт повтора.
QPACK решает это разделением: вставки в динамическую таблицу передаются по отдельным, специально выделенным однонаправленным потокам (encoder stream и decoder stream), а не вперемешку с обычными запросами. Заголовочный блок конкретного запроса может сослаться на запись динамической таблицы, но получатель обязан явно дождаться, пока нужная вставка действительно доедет — если пакет с ней потерялся, обработка этого блока блокируется («blocked stream»), но остальные потоки соединения продолжают идти своим чередом. Строгий порядок доставки внутри одного потока гарантирует сам QUIC, а согласованность между разными потоками реализуется явным ожиданием, а не полагается на порядок пакетов на уровне транспорта.
Кроме этого, в QPACK статическая таблица расширена (99 записей вместо 61 в HPACK), а формат индексов и логика вставки/эвикции переработаны под асинхронную доставку. Но идея та же самая: не передавать текстом то, что уже известно обеим сторонам, а слать компактную ссылку на общую память.
Почему выигрыш особенно заметен на страницах с десятками ресурсов
Экономия от индексирования заголовков считается не в процентах от общего трафика сайта, а per-запрос — и именно поэтому она так по-разному ощущается в зависимости от того, что вы грузите. Скачиваете дистрибутив на пару гигабайт одним запросом — заголовки в любом виде исчезающе малая доля трафика, тут сжимать особо нечего. А вот если страница состоит из полусотни мелких запросов — иконки, шрифты, куски CSS и JS, десяток вызовов к API за данными для виджетов, — то для каждого из них соотношение «заголовки против полезной нагрузки» смещено далеко не в пользу нагрузки, и именно здесь замена текста на индекс даёт относительно наибольший эффект.
Современные веб-приложения тяготеют именно к такому профилю: много мелких сущностей вместо нескольких тяжёлых. Одностраничные приложения дробят интерфейс на десятки независимых API-вызовов, картинки режутся на несколько размеров под разные экраны, шрифты и иконки подгружаются по требованию. Для каждого из этих запросов заголовки — почти всегда один и тот же набор с одними и теми же значениями, а тело ответа может быть совсем небольшим. Именно комбинация «много запросов» плюс «маленькое тело каждого» превращает сжатие заголовков из приятной мелочи в заметную часть общей эффективности протокола.
Есть и вторая сторона: экономия работает не только на объём передачи, но и на задержку. Меньше байт на заголовки — меньше пакетов нужно собрать и обработать, особенно на не самом быстром или не самом стабильном канале, где каждый лишний пакет — это дополнительный шанс на потерю и повторную передачу (о том, почему единичная потеря пакета обходится непропорционально дорого, стоит прочитать отдельно). Здесь же работает и то, что динамическая таблица накапливает пользу только в рамках живого соединения: чем дольше используется одно и то же TCP- или QUIC-соединение вместо открытия нового на каждый запрос, тем больше заголовков успевает попасть в таблицу и тем чаще применяются короткие индексы вместо текста — ещё один аргумент за переиспользование соединений и разумные таймауты keep-alive.
Точную цифру экономии в процентах от трафика конкретного сайта без измерений дать нельзя — она зависит от числа запросов на странице, разнообразия заголовков (в первую очередь от того, насколько предсказуем Cookie) и от того, как часто клиент переоткрывает соединения. Но направление эффекта устойчиво: чем мельче и многочисленнее запросы, тем больше в относительном выражении даёт замена повторяющегося текста на индекс.
Что это значит для настройки вашего сервера
Хорошая новость: HPACK и QPACK — обязательная часть HTTP/2 и HTTP/3, и в подавляющем большинстве случаев работают «из коробки», без отдельной настройки на вашей стороне. Если на сервере включён HTTP/2 или HTTP/3, сжатие заголовков включается автоматически как часть протокола — отдельного флага «включить HPACK» в nginx или другом веб-сервере нет, потому что без него нет самого протокола.
Убедиться, что HTTP/2 действительно работает, можно так:
curl -v --http2 https://ваш-домен/ 2>&1 | grep -i "using http"
Для HTTP/3 в современном nginx (сборки с поддержкой QUIC) конфигурация выглядит примерно так — стоит свериться с документацией под конкретную версию, которая у вас установлена, потому что директивы для HTTP/3 стабилизировались не так давно и могли измениться между релизами:
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
http3 on;
ssl_protocols TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400';
}
Что действительно стоит проконтролировать вручную:
- Не раздувайте куки. Огромный
Cookieс десятком трекинговых значений почти никогда не попадёт целиком в статическую таблицу и будет медленно «прогреваться» в динамической, особенно если значение чуть меняется от запроса к запросу (например, содержит таймстамп). Чем компактнее и стабильнее куки, тем лучше работает сжатие. - Проверьте размер буферов заголовков. Директивы вида
large_client_header_buffersв nginx ограничивают, сколько заголовочных данных сервер готов принять; если у вас нестандартно большие заголовки (объёмные JWT в Authorization), убедитесь, что лимиты не режут легитимные запросы. - Держите соединения живыми там, где это уместно. Динамическая таблица — память конкретного соединения; она бесполезна, если каждый запрос идёт по новому соединению. Разумные таймауты
keepalive_timeoutи переиспользование соединений между прокси и приложением увеличивают долю запросов, которые пользуются накопленным словарём. - Мониторьте через DevTools, а не только логи сервера. На вкладке Network включите колонку Protocol и убедитесь, что запросы к статике идут по h2 или h3, а не откатываются на HTTP/1.1 — откат случается, если TLS-терминация настроена без поддержки нужного протокола или неправильно обрабатывает SNI на сервере с несколькими доменами на одном IP.
Если вы арендуете VPS под сайт с большим количеством мелких запросов — SPA с активным API, каталог с сотнями превью-картинок, — включённые HTTP/2 или HTTP/3 на балансировщике или прокси перед приложением обычно уже дают заметную часть выигрыша от сжатия заголовков без дополнительных действий с вашей стороны, кроме корректной настройки TLS и самого прокси.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Нужно ли мне что-то специально настраивать, чтобы включить HPACK или QPACK?
Нет, это неотъемлемая часть протокола: если работает HTTP/2, работает и HPACK, если работает HTTP/3 — работает QPACK. Отдельного переключателя не существует.
Может ли сжатие заголовков сломать что-то в моём приложении?
Само индексирование прозрачно для приложения — оно получает те же заголовки, что отправлялись, просто иначе закодированные на уровне транспорта. Проблемы обычно возникают не из-за HPACK/QPACK, а из-за прокси или библиотек, которые неправильно реализуют протокол — это редкость в зрелом стеке вроде nginx или современных браузеров.
Даёт ли HPACK какой-то выигрыш, если у меня HTTP/1.1?
Нет, HPACK и QPACK — часть спецификаций HTTP/2 и HTTP/3 соответственно, в HTTP/1.1 такого механизма нет вообще. Единственный способ получить эту экономию — перейти на более новую версию протокола.
Стоит ли специально уменьшать число заголовков ради экономии трафика?
Как разовая мера — вряд ли: динамическая таблица и так съедает большую часть повторов. Но явно избыточные или дублирующиеся кастомные заголовки убрать полезно в любом случае, независимо от версии протокола.
Работает ли сжатие заголовков между разными доменами?
Нет, динамическая таблица привязана к конкретному TCP- или QUIC-соединению, а соединение — к конкретному origin. Разные сайты в разных вкладках не делятся друг с другом накопленным словарём.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →