Node.js Internals

HTTP/2 & HTTP/3: Современные протоколы

Современный веб-сайт делает 150 HTTP запросов на одну страницу. В HTTP/1.1 это катастрофа - браузер открывает 6 TCP соединений, каждое с handshake задержкой 300ms (mobile 4G). Запросы выстраиваются в очереди, один медленный API блокирует загрузку всех остальных. Пользователь видит белый экран 4 секунды. В ход идут костыли: domain sharding, CSS sprites, inline base64 images - но это усложняет деплой и ломает кэширование. HTTP/2 решает проблему в корне: один TCP handshake, мультиплексирование 150 запросов параллельно, сжатие заголовков. HTTP/3 идёт дальше - устраняет TCP head-of-line blocking через QUIC, 0-RTT reconnect для мобильных. Это не просто «новая версия протокола» - это переосмысление того, как работает современный веб.

  • **Mobile web performance:** Пользователь на 4G с 200ms latency открывает интернет-магазин. HTTP/1.1: 6 connections × 200ms handshake = 1.2s до первого запроса, head-of-line blocking добавляет ещё 2s → total 3.2s белый экран. HTTP/2: 1 connection × 200ms = 200ms, мультиплексирование 100 запросов → 1s до full render. HTTP/3 + 0-RTT: returning user → 0ms handshake → 600ms total. Конверсия выросла на 15% после миграции на HTTP/2 + 103 Early Hints
  • **Real-time dashboard с WebSocket + REST API:** Dashboard подписывается на WebSocket для live updates + делает 50 REST API запросов для загрузки данных. HTTP/1.1: WebSocket занимает одно из 6 connections → только 5 осталось для API → загрузка 10s. HTTP/2: WebSocket + 50 API запросов идут параллельно по одному мультиплексированному соединению → загрузка 2s. HTTP/3: connection migration при переключении Wi-Fi → mobile → WebSocket не рвётся, нет reconnect lag
  • **CDN для streaming video:** Video chunks (2s segments) загружаются через progressive download. HTTP/1.1: каждый chunk = новый request, head-of-line blocking между chunks → buffering при медленных сегментах. HTTP/2: chunks мультиплексируются, приоритизация через PRIORITY frames → критичные chunks (начало видео) загружаются первыми. HTTP/3: packet loss на mobile не блокирует следующие chunks (независимые QUIC streams) → smooth playback на 4G

Эволюция HTTP: от 1.1 к 3.0

**HTTP/1.1 - это протокол 1999 года, который до сих пор используется повсеместно.** Но веб изменился радикально: современная страница загружает 100+ ресурсов (JS, CSS, изображения, шрифты, API calls), а HTTP/1.1 был спроектирован для простых HTML-документов с парой картинок. Главная проблема - **head-of-line blocking**: один медленный запрос блокирует все последующие в очереди.

Сценарий: браузер запрашивает 6 файлов одновременно по одному TCP соединению. HTTP/1.1 требует, чтобы ответы приходили **строго по очереди**. Если первый файл (script.js) тормозит на сервере 2 секунды, остальные 5 файлов ждут, хотя готовы мгновенно. Решение HTTP/1.1 - открыть 6 параллельных TCP соединений (браузеры ограничивают до 6 на домен). Но это костыль: каждое соединение = TCP handshake (1 RTT) + TLS handshake (2 RTT) = 3 RTT задержка перед первым байтом.

**Head-of-line blocking в HTTP/1.1:** Запросы/ответы идут последовательно. Если response 1 задерживается, response 2-6 ждут, даже если готовы. **Workaround:** браузеры открывают 6 TCP соединений на домен. **Проблема workaround:** TCP slow start на каждое соединение, высокий overhead на handshakes. **HTTP/2 решение:** мультиплексирование - все запросы идут параллельно по одному TCP соединению.

**Хронология эволюции HTTP:** HTTP/1.0 (1996) → одно соединение на запрос. HTTP/1.1 (1999) → keep-alive, pipelining (но не работает из-за HOL blocking). HTTP/2 (2015) → мультиплексирование, бинарный протокол, header compression. HTTP/3 (2022) → QUIC вместо TCP, 0-RTT handshake, независимые потоки без HOL blocking на транспортном уровне.

В чём главная проблема HTTP/1.1 при загрузке 100 ресурсов?

Мультиплексирование и бинарный фрейминг

**HTTP/2 переизобретает передачу данных на фундаментальном уровне.** Вместо текстовых запросов/ответов HTTP/2 использует **бинарный фрейминг** - данные разбиваются на маленькие frames (DATA, HEADERS, PRIORITY), которые передаются **параллельно по множеству потоков** внутри одного TCP соединения. Каждый поток (stream) - это независимый запрос/ответ, frames разных потоков **чередуются** (interleaving).

**Бинарный фрейм:** Минимальная единица данных в HTTP/2. Состоит из: заголовка фрейма (9 байт: length, type, flags, stream ID) + payload. **Типы frames:** HEADERS (заголовки запроса/ответа), DATA (тело), PRIORITY (приоритет потока), RST_STREAM (отмена), SETTINGS (настройки соединения), PING (keep-alive). **Stream:** Логический канал для одного запрос-ответ. Stream ID - нечётный для client-initiated, чётный для server push.

**Приоритизация потоков (stream prioritization):** Клиент может указать вес (weight 1-256) и зависимость (dependency) потоков. Например: CSS weight=200, JS weight=150, images weight=50 → сервер отправит CSS frames чаще. Зависимость: «не отправляй JS пока не загрузится CSS». На практике приоритизация сложная и часто игнорируется серверами.

**HPACK - сжатие заголовков:** HTTP/1.1 отправляет одинаковые заголовки (User-Agent, Cookie) с каждым запросом. Для 100 запросов это ~50KB overhead. HTTP/2 использует HPACK: заголовки сжимаются через Huffman encoding + индексация. Первый запрос отправляет полные заголовки → создаётся таблица индексов. Последующие запросы: «заголовки как в индексе #5». Экономия: ~80% размера headers.

Что позволяет HTTP/2 избежать head-of-line blocking?

Server Push и HPACK сжатие

**Server Push - это возможность сервера отправить ресурсы клиенту ДО того, как клиент их запросил.** Классический сценарий: клиент запрашивает `/index.html`. Сервер знает, что HTML ссылается на `style.css` и `app.js`. Вместо ожидания двух дополнительных запросов, сервер **пушит** CSS и JS сразу с HTML. Экономия: 1 RTT на каждый pushed ресурс.

**Как работает Server Push:** 1) Клиент запрашивает `/index.html` (stream 1) 2) Сервер отправляет PUSH_PROMISE frame: «Я отправлю тебе `/style.css` через stream 2» 3) Сервер начинает отправлять HEADERS + DATA для stream 2 ДО того, как клиент его запросил 4) Браузер получает pushed ресурс и кладёт в кэш 5) Когда HTML парсится и встречает `<link rel=stylesheet>`, браузер берёт CSS из кэша вместо нового запроса.

**Проблемы Server Push на практике:** 1) **Cache invalidation is hard** - сервер не знает, есть ли уже ресурс в browser cache. Может push'ить то, что уже есть (wasted bandwidth) 2) **Over-pushing** - сервер push'ит 10 файлов, а юзер закрыл страницу через 1 секунду (wasted CPU/network) 3) **Сложность реализации** - нужна логика определения, что push'ить. Из-за этих проблем **Server Push практически не используется** и удалён из Chrome с 2022 года. Вместо него - `103 Early Hints` (HTTP status code).

**HPACK детали:** Динамическая таблица (4KB по умолчанию) + статическая таблица (61 предопределённый header). Пример: первый запрос отправляет `User-Agent: Mozilla/5.0...` (200 байт) → индекс #15 в таблице. Следующие запросы: просто число `15` вместо 200 байт. Huffman encoding дополнительно сжимает строки на ~30%. В сумме: заголовки уменьшаются в 5-10 раз для типичных веб-приложений.

Почему Server Push был удалён из Chrome в 2022 году?

HTTP/3 и QUIC: UDP революция

**HTTP/3 решает последнюю оставшуюся проблему HTTP/2: head-of-line blocking на уровне TCP.** HTTP/2 убрал HOL blocking на уровне приложения (через мультиплексирование streams), но TCP **всё ещё** требует доставки пакетов по порядку. Если пакет #5 потерялся в сети, TCP блокирует доставку пакетов #6, #7, #8 приложению, пока #5 не будет retransmit. Все HTTP/2 streams замораживаются из-за одного потерянного TCP packet!

**QUIC (Quick UDP Internet Connections):** Транспортный протокол от Google, работает поверх UDP вместо TCP. **Ключевые фичи:** 1) **Независимые потоки** - потеря пакета в stream 1 не блокирует stream 3 2) **0-RTT handshake** - повторное подключение к серверу занимает 0 RTT (данные отправляются сразу) 3) **Встроенный TLS 1.3** - шифрование на уровне транспорта 4) **Connection migration** - смена IP/порта без разрыва соединения (переключение Wi-Fi → mobile) 5) **Better congestion control** - улучшенные алгоритмы по сравнению с TCP.

**0-RTT handshake magic:** Традиционно: TCP handshake (SYN, SYN-ACK, ACK = 1 RTT) + TLS handshake (ClientHello, ServerHello, keys = 1-2 RTT) = **2-3 RTT перед первым байтом данных**. QUIC: при первом подключении - 1 RTT (combined crypto + transport handshake). При повторном подключении (resume) - **0 RTT**: клиент отправляет данные сразу с первым пакетом, используя сохранённые ключи. Для мобильных приложений с частыми переподключениями это критично.

**Adoption HTTP/3:** Google (все сервисы), Facebook, Cloudflare, Fastly. ~30% топ-10K сайтов поддерживают HTTP/3 (2024). Браузеры: Chrome/Edge (стабильно), Firefox (стабильно), Safari (beta). Node.js: экспериментальная поддержка через флаги, production-ready библиотеки появляются (cloudflare/quiche bindings). Проблемы: UDP блокируется на некоторых корпоративных файрволах → fallback на HTTP/2.

Как HTTP/3 (QUIC) решает TCP head-of-line blocking?

Миграция и практическое применение

**Миграция с HTTP/1.1 на HTTP/2 - это не просто включение флага.** Нужно переосмыслить паттерны оптимизации, которые были костылями для HTTP/1.1, но вредят в HTTP/2. **Антипаттерны HTTP/1.1, которые нужно убрать:** 1) **Domain sharding** (assets1.cdn.com, assets2.cdn.com) - создавали для обхода лимита 6 соединений. В HTTP/2 это замедляет: теряется мультиплексирование, multiple TCP handshakes 2) **Sprite images** - склейка иконок в один файл. В HTTP/2 дешевле загрузить 50 мелких иконок параллельно 3) **Inlining CSS/JS в HTML** - увеличивает размер HTML, убивает кэширование. HTTP/2 загрузит отдельные файлы за тот же RTT.

**Checklist миграции на HTTP/2:** 1) **TLS обязателен** - браузеры требуют HTTPS для HTTP/2 (хотя спецификация разрешает h2c - cleartext) 2) **Reverse proxy** - Nginx, Caddy, HAProxy поддерживают HTTP/2 out-of-box. Node.js приложение может работать на HTTP/1.1 за прокси 3) **Убрать domain sharding** - один домен для всех assets 4) **Разделить bundle** - вместо 1MB app.js → 10 файлов по 100KB (параллельная загрузка + лучше кэширование) 5) **103 Early Hints** вместо Server Push 6) **Мониторинг** - проверить, что клиенты реально используют HTTP/2 через `req.httpVersion`.

**Performance comparison (типичный веб-сайт, 100 ресурсов, 200ms RTT):** HTTP/1.1 (6 connections): ~12 RTT = 2.4s. HTTP/2 (1 connection, multiplexing): ~4 RTT = 800ms (3x faster). HTTP/3 (QUIC, 0-RTT resume): ~2 RTT = 400ms (6x faster для returning users). Реальный мир: HTTP/2 даёт 10-30% улучшение median load time, HTTP/3 даёт дополнительные 5-15% на мобильных сетях с packet loss.

HTTP/2 автоматически ускоряет любое приложение без изменений кода

HTTP/2 требует пересмотра оптимизаций - domain sharding, sprites, inlining вредят. Нужно адаптировать архитектуру

Паттерны HTTP/1.1 (domain sharding, CSS sprites, inline assets) были workarounds для head-of-line blocking и лимита 6 connections. В HTTP/2 эти «оптимизации» становятся антипаттернами: domain sharding создаёт лишние TCP handshakes, sprites убивают параллелизм, inlining ломает кэширование. Нужно разделить bundles, убрать sharding, использовать отдельные файлы - тогда HTTP/2 даст полный эффект

Почему domain sharding (assets1.cdn.com, assets2.cdn.com) - антипаттерн для HTTP/2?

Ключевые идеи

  • **HTTP/1.1 страдает от head-of-line blocking:** запросы обрабатываются последовательно, медленный response блокирует всю очередь. Workaround (6 parallel TCP connections) неэффективен: каждое соединение = 3 RTT handshake (TCP + TLS), TCP slow start на каждое. HTTP/2 решает через бинарное мультиплексирование: все запросы идут параллельно streams по одному TCP соединению
  • **HTTP/2 оптимизации:** HPACK сжатие заголовков (80% экономия), приоритизация streams, Server Push (устарел, заменён на 103 Early Hints). Миграция требует убрать HTTP/1.1 антипаттерны: domain sharding, CSS sprites, inlining. Лучшая практика: разделить bundles, один домен, TLS обязателен, reverse proxy (Nginx) для HTTP/2 termination
  • **HTTP/3 (QUIC) устраняет TCP head-of-line blocking:** работает поверх UDP, независимые streams на транспортном уровне. Потеря пакета в stream 1 не блокирует stream 3. Дополнительные фичи: 0-RTT handshake (мгновенный reconnect), connection migration (переключение сетей без разрыва), built-in TLS 1.3. Adoption растёт: 30% топ-сайтов, все major CDN. Node.js поддержка экспериментальная, production через библиотеки

Связанные темы

HTTP/2 и HTTP/3 связаны со всеми аспектами веб-архитектуры: от низкоуровневого networking до высокоуровневых паттернов оптимизации:

  • TLS & Security — HTTP/2 требует TLS в браузерах (ALPN negotiation), HTTP/3 встраивает TLS 1.3 в QUIC. 0-RTT handshake требует понимания replay attack mitigations
  • Streams API — HTTP/2 streams в Node.js реализованы через Duplex streams. Response streaming критичен для Server Push и chunked transfer
  • Performance & Profiling — Waterfall charts показывают HTTP/2 мультиплексирование vs HTTP/1.1 blocking. Connection timing (TTFB, DNS, TCP, TLS) критичен для оптимизации

Вопросы для размышления

  • Если mobile 4G имеет 200ms RTT и 5% packet loss, насколько быстрее будет загрузка 100 ресурсов в HTTP/3 vs HTTP/2? Учитывайте 0-RTT handshake и независимые QUIC streams при packet loss
  • Почему Server Push был удалён из Chrome, но 103 Early Hints работает? Какая фундаментальная разница в подходе - кто принимает решение о загрузке ресурса?
  • API сервер стоит за Nginx reverse proxy. Клиент подключается по HTTP/2 к Nginx, Nginx проксирует запросы к Node.js по HTTP/1.1. Получает ли клиент преимущества HTTP/2 мультиплексирования? Где возникает bottleneck?

Связанные уроки

  • net-21-http-basics
HTTP/2 & HTTP/3: Современные протоколы

0

1

Войти