HTTP/3 i QUIC - dlaczego Twoja aplikacja jest wolniejsza niż mogłaby

Użytkownik w Bangalore otwiera Twoją aplikację hostowaną we Frankfurcie. Kliknie przycisk, czeka. Kliknie drugi raz, bo myśli, że pierwszy nie zadziałał. Teraz masz dwa żądania, dwa spinnery i jednego sfrustrowanego klienta.

Nie, to nie jest problem z Twoim kodem. To jest problem z protokołem, przez który Twój kod podróżuje.

HTTP/2 miał to naprawić. I naprawił - częściowo. Ale zostawił jedną bombę zegarową, która tyka przy każdym zgubionym pakiecie: head-of-line blocking na warstwie TCP. HTTP/3 tę bombę rozbraja. Całkowicie. Bo wyrzuca TCP i zastępuje go czymś innym.

TCP ma 50 lat. I to widać.

TCP (Transmission Control Protocol) powstał w 1974 roku. Działa niezawodnie - każdy pakiet dociera, w kolejności, albo jest retransmitowany. Problem? Gdy jeden pakiet się zgubi, TCP blokuje wszystkie dane za nim. Nawet te, które dotarły bezbłędnie.

W HTTP/2 to wygląda tak:

Przeglądarka pobiera równolegle 6 zasobów przez jedno połączenie TCP:

Stream 1: style.css     ████████░░ (czeka)
Stream 2: app.js        ████████░░ (czeka)
Stream 3: logo.png      ████████░░ (czeka)
Stream 4: font.woff2    ████████░░ (czeka)
Stream 5: data.json     ████░░░░░░ (zgubiony pakiet!)
Stream 6: analytics.js  ████████░░ (czeka)

TCP: "Pakiet ze Stream 5 nie dotarł. STOP. Wszyscy czekają
      na retransmisję. Nikogo nie przepuszczam."

Czas blokady: 1 RTT (round-trip time) = 50-300ms
              zależnie od odległości do serwera

Pięć strumieni ma kompletne dane. Mogłyby być przetworzone. Ale TCP ich nie przepuści, bo jeden pakiet z szóstego strumienia się zgubił. To jest head-of-line blocking - i to jest powód, dla którego HTTP/2 na stratnym łączu potrafi być wolniejszy niż HTTP/1.1 z sześcioma osobnymi połączeniami.

QUIC - nowy transport, który rozumie strumienie

QUIC (Quick UDP Internet Connections) to protokół transportowy zbudowany na UDP. Ale to nie jest "UDP z ficzerami". To kompletny, niezawodny transport z szyfrowanie wbudowanym na poziomie protokołu - nie jako osobna warstwa TLS na wierzchu.

Kluczowa różnica: QUIC wie o strumieniach. Zgubiony pakiet blokuje tylko strumień, do którego należał:

Te same 6 zasobów, ale przez QUIC:

Stream 1: style.css     ██████████ ✓ gotowy, przetwarzany
Stream 2: app.js        ██████████ ✓ gotowy, przetwarzany
Stream 3: logo.png      ██████████ ✓ gotowy, przetwarzany
Stream 4: font.woff2    ██████████ ✓ gotowy, przetwarzany
Stream 5: data.json     ████░░░░░░ ⏳ retransmisja pakietu
Stream 6: analytics.js  ██████████ ✓ gotowy, przetwarzany

QUIC: "Stream 5 czeka na retransmisję.
       Reszta? Proszę, działajcie."

Pięć z sześciu zasobów jest przetwarzanych natychmiast. Użytkownik widzi stronę. Tylko data.json dociąga się z opóźnieniem. Różnica? Na stabilnym łączu - minimalna. Na WiFi w kawiarni, LTE w pociągu, sieci firmowej z 2% packet loss - dramatyczna.

Handshake - gdzie HTTP/3 wygrywa, zanim wyślesz pierwsze dane

Zanim przeglądarka pobierze pierwszy bajt, musi nawiązać połączenie. I tu HTTP/3 robi rzecz, której HTTP/2 nie potrafi - łączy handshake transportowy z kryptograficznym w jedno:

EtapHTTP/2 (TCP + TLS 1.3)HTTP/3 (QUIC)
Transport handshake1 RTT (TCP SYN/ACK)0 RTT (w QUIC)
Crypto handshake1 RTT (TLS 1.3)Razem z transportem
Pierwsze danePo 2 RTTPo 1 RTT
Połączenie wznowione (0-RTT)1 RTT (TCP) + 0 RTT (TLS) = 1 RTT0 RTT

Przy pierwszym połączeniu: HTTP/3 potrzebuje 1 RTT zamiast 2. Przy wznowionym połączeniu (użytkownik wraca na stronę): 0 RTT - dane lecą w pierwszym pakiecie, zanim serwer odpowie na handshake.

Co to oznacza w praktyce?

TrasaRTTOszczędność 1 RTTOszczędność 2 RTT (0-RTT)
Warszawa → Frankfurt~20 ms20 ms40 ms
Warszawa → Londyn~30 ms30 ms60 ms
Warszawa → Nowy Jork~90 ms90 ms180 ms
Warszawa → Singapur~180 ms180 ms360 ms
Warszawa → Sydney~300 ms300 ms600 ms

Dla użytkownika w Singapurze Twoja aplikacja staje się 180 ms szybsza przy każdym nowym połączeniu. Przy 0-RTT - 360 ms szybsza. To różnica między "kliknąłem i jest" a "kliknąłem i czekam".

Benchmarki - konkrety zamiast obietnic

Testy Request Metrics (Caddy, TLS 1.3/0-RTT, 20 iteracji na test):

Mała strona (20 zasobów, 600 KB)

LokalizacjaHTTP/2HTTP/3Różnica
Nowy Jork (bliski serwer)~800 ms~600 ms200 ms szybciej
Londyn~1400 ms~800 ms600 ms szybciej
Bangalore~2200 ms~1600 ms600 ms szybciej

Średnia strona (105 zasobów, 10 MB)

LokalizacjaHTTP/2HTTP/3Różnica
Nowy Jork~2800 ms~2475 ms325 ms szybciej
Londyn~5200 ms~4000 ms1200 ms szybciej
Bangalore~8500 ms~6500 ms2000 ms szybciej

SPA (115 zasobów, 15 MB)

LokalizacjaHTTP/2HTTP/3Różnica
Nowy Jork~3500 ms~3200 ms300 ms szybciej
Londyn~6000 ms~5000 ms1000 ms szybciej
Bangalore~10000 ms~7500 ms2500 ms szybciej

Wzorzec jest jasny: im dalej od serwera, tym większy zysk z HTTP/3. Dla użytkowników lokalnych - różnica jest niewielka. Dla użytkowników globalnych - sekundy.

Packet loss - tu QUIC dominuje

Testy Cloudflare i badania akademickie:

Packet lossHTTP/2 vs HTTP/3
0%HTTP/2 ≈ HTTP/3 (różnica < 5%)
1%HTTP/3 14.5% szybszy
2%HTTP/3 ~30% szybszy
4%HTTP/3 ~50% szybszy
10%HTTP/3 do 88% szybszy

Przy 0% strat - nie ma różnicy. Ale kto ma 0% strat? Twoje łącze w biurze - może. WiFi w hotelu? LTE w metrze? Sieć firmowa w piątek po południu, gdy ktoś odpala backup?

Realistyczny packet loss na WiFi to 1-3%. Na LTE - 0.5-2%. Na przesyconym łączu firmowym - bywa i 5%. W tych warunkach HTTP/3 nie jest "trochę lepszy". Jest fundamentalnie lepszy.

Co to zmienia dla WebSocketów i Phoenix LiveView

Tu zaczyna się ciekawa historia. Phoenix LiveView używa WebSocketów do komunikacji w czasie rzeczywistym. WebSocket działa na TCP. A TCP ma ten sam problem co HTTP/2 - head-of-line blocking.

Scenariusz: LiveView wysyła 10 aktualizacji DOM na sekundę. Jeden pakiet się gubi. TCP blokuje wszystkie kolejne aktualizacje, dopóki zgubiony pakiet nie zostanie retransmitowany. Użytkownik widzi "zamrożony" interfejs na 50-300 ms. Przy 2% packet loss - to się zdarza kilka razy na minutę.

WebSocket dzisiaj (TCP)

Aktualizacja 1  ████ → przeglądarka (ok)
Aktualizacja 2  ████ → przeglądarka (ok)
Aktualizacja 3  ██░░ → ZGUBIONY PAKIET
Aktualizacja 4  ████ → bufor TCP (czeka)
Aktualizacja 5  ████ → bufor TCP (czeka)
Aktualizacja 6  ████ → bufor TCP (czeka)
                       ... 100-200ms ciszy ...
Retransmisja 3  ████ → przeglądarka (ok)
Aktualizacja 4  ████ → przeglądarka (wreszcie)
Aktualizacja 5  ████ → przeglądarka (wreszcie)
Aktualizacja 6  ████ → przeglądarka (wreszcie)

Użytkownik: interfejs zamrożony na 100-200 ms, potem nagle "skok" - 4 aktualizacje na raz.

WebTransport - następca WebSocketów

WebTransport to nowy protokół zbudowany na QUIC, zaprojektowany jako następca WebSocketów. Kluczowe różnice:

CechaWebSocketWebTransport
TransportTCPQUIC (UDP)
Head-of-line blockingTak - jeden zgubiony pakiet blokuje wszystkoNie - strumienie są niezależne
Strumienie1 dwukierunkowyWiele niezależnych
Tryb unreliableBrakDatagramy - dane bez gwarancji dostarczenia
Migracja sieciNie - zmiana IP = zerwane połączenieTak - przeżywa zmianę WiFi → LTE
0-RTTNieTak
Wsparcie przeglądarek99%+~75% (Chrome, Edge; Firefox za flagą; Safari brak)

Dla LiveView tryb unreliable to rewolucja. Aktualizacja pozycji kursora, animacja, wskaźnik "ktoś pisze..." - to dane, które nie muszą docierać niezawodnie. Jeśli jedna aktualizacja pozycji kursora się zgubi, następna ją nadpisze. Nie ma sensu retransmitować.

Stan wsparcia w ekosystemie Elixir

Bandit (HTTP serwer napisany w Elixirze) obsługuje dziś HTTP/1.x, HTTP/2 i WebSocket. HTTP/3 i WebTransport? Matt Trudel (maintainer Bandit) wspomniał o planach na QUIC po wersji 1.0, z WebTransport jako następnym krokiem.

Phoenix 1.7 celowo wydzielił obsługę WebSocket do biblioteki WebSock - właśnie po to, żeby w przyszłości dodać WebTransport bez zmian w rdzeniu Phoenixa.

Porownanie stosow Phoenix LiveView: obecny (WebSock -> Bandit -> TCP) vs przyszly (WebSock -> WebSocket+WebTransport -> TCP+QUIC)

WebSock abstrahuje transport - dodanie WebTransport nie wymaga zmian w Phoenix LiveView ani Channels.

Architektura jest gotowa na tę zmianę. Brakuje implementacji QUIC w Erlangu/Elixirze - ale fundament jest.

Adopcja - gdzie jesteśmy

Przeglądarki - gotowe

PrzeglądarkaHTTP/3WebTransport
ChromeOd wersji 87 (2020)Od wersji 97 (2022)
EdgeOd wersji 87 (2020)Od wersji 97 (2022)
FirefoxOd wersji 88 (2021)Za flagą
SafariOd wersji 16 (2022)Brak

100% głównych przeglądarek obsługuje HTTP/3. WebTransport - na razie Chrome i Edge.

Serwery - tu jest problem

SerwerHTTP/3
CaddyPełne wsparcie (domyślnie włączone)
NginxEksperymentalne (wyłączone domyślnie)
ApacheBrak wsparcia, brak planów
Node.js (stdlib)Brak wsparcia
Go (stdlib)Brak wsparcia
Bandit (Elixir)Brak wsparcia (planowane)
Cowboy (Erlang)Brak wsparcia

CDN i reverse proxy - najłatwiejsza ścieżka

UsługaHTTP/3
CloudflareTak (domyślnie)
AWS CloudFrontTak
Google Cloud CDNTak
FastlyTak
AkamaiTak

I to jest najważniejszy wniosek praktyczny: nie musisz mieć serwera z HTTP/3. Wystarczy CDN lub reverse proxy z HTTP/3 przed Twoim serwerem HTTP/2. Cloudflare → Twój Bandit/Nginx. Użytkownik łączy się po QUIC z Cloudflare, Cloudflare łączy się po TCP z Twoim serwerem. Zysk z 0-RTT i eliminacji HOL blocking - jest. Zero zmian w aplikacji.

Schemat: Użytkownik łączy się po QUIC/HTTP/3 z CDN (Cloudflare/Caddy), CDN łączy się po TCP/HTTP/2 z serwerem aplikacji (Bandit/Cowboy)

Użytkownik dostaje HTTP/3 z CDN. Twój serwer dalej mówi po TCP. Zero zmian w kodzie.

Globalny ruch - HTTP/3 jest już tu

MetrykaWartość (Q1 2026)
Strony obsługujące HTTP/3~31-36% (w3techs)
Ruch przez QUIC (Cloudflare)~35% globalnego ruchu webowego
Wsparcie przeglądarek100% głównych
Udział mobilnyHTTP/3 na ~30% ruchu mobilnego

To nie jest eksperyment. Google, Meta, Cloudflare, Akamai - obsługują większość swojego ruchu przez HTTP/3. YouTube, Gmail, Google Search, Facebook, Instagram - to wszystko działa na QUIC. Jeśli Twoja aplikacja tego nie robi - Twoi użytkownicy mają gorsze doświadczenie niż na YouTube.

Jak włączyć HTTP/3 - praktyczny plan

Opcja 1: CDN (dziś, 30 minut)

Najszybsza ścieżka. Zero zmian w aplikacji.

  1. Wrzuć aplikację za Cloudflare (lub inny CDN z HTTP/3)
  2. Włącz HTTP/3 w panelu CDN (w Cloudflare jest domyślnie włączone)
  3. Gotowe - przeglądarka automatycznie użyje HTTP/3
# Sprawdź, czy Twoja strona obsługuje HTTP/3
curl -sI https://twoja-strona.pl | grep -i alt-svc

# Powinieneś zobaczyć:
# alt-svc: h3=":443"; ma=86400

Nagłówek alt-svc mówi przeglądarce: "hej, umiem HTTP/3 na porcie 443, spróbuj". Przeglądarka przy następnym żądaniu przełączy się na QUIC.

Opcja 2: Caddy jako reverse proxy (dziś, 1 godzina)

Caddy obsługuje HTTP/3 domyślnie. Stawiasz go przed Bandit/Cowboy:

# Caddyfile
twoja-strona.pl {
    reverse_proxy localhost:4000
}

# To wszystko. Caddy automatycznie:
# - Pobierze certyfikat Let's Encrypt
# - Włączy HTTP/3
# - Ustawi alt-svc header
# - Obsłuży 0-RTT

Opcja 3: Natywne wsparcie w serwerze (przyszłość)

Czekamy na:

  • Bandit z QUIC (planowane)
  • Erlang/OTP z natywnym QUIC (w trakcie eksploracji)
  • Nginx z stabilnym HTTP/3 (w trakcie)

Na dziś - opcja 1 lub 2 daje 90% korzyści bez żadnych zmian w kodzie.

Wady i ograniczenia - bo nie jest różowo

1. UDP i firewalle korporacyjne

QUIC działa na UDP. Wiele firewalli korporacyjnych blokuje UDP na porcie 443 - bo do tej pory tylko TCP tam chodził. Efekt? Użytkownik za korporacyjnym firewallem nie połączy się po HTTP/3. Przeglądarka spadnie na HTTP/2 (fallback jest automatyczny) - ale tracisz korzyści właśnie tam, gdzie packet loss jest najwyższy - w sieciach korporacyjnych.

2. Debugowanie jest trudniejsze

Wireshark rozumie TCP od 30 lat. QUIC? Ruch jest szyfrowany na poziomie transportu - nie tylko dane, ale też nagłówki pakietów. tcpdump widzi UDP i nic więcej. Potrzebujesz kluczy sesji (SSLKEYLOGFILE) żeby cokolwiek zdekodować. Narzędzia istnieją - ale krzywa uczenia jest stroma.

3. Serwery nie nadążają

Jak pokazała tabela wyżej - Apache nie ma planów, Nginx ma "eksperymentalne" wsparcie, a standardowe biblioteki Go, Node.js, Rust i Erlanga nie obsługują QUIC. Powód? OpenSSL przez lata nie miał kompatybilnego API dla QUIC. BoringSSL (fork Google) miał - od 2018. OpenSSL? Wsparcie po stronie serwera dopiero od 2025. To spowolniło cały ekosystem open source o 7 lat.

4. 0-RTT i replay attacks

0-RTT jest szybki, ale ma cenę: dane wysłane w trybie 0-RTT nie mają forward secrecy i są podatne na replay attacks. Atakujący może przechwycić pakiet 0-RTT i wysłać go ponownie. Dla żądań GET (idempotentnych) - to nie problem. Dla POST (zmiana stanu) - to jest problem. Dlatego serwery powinny odrzucać nieidempotentne żądania w 0-RTT. Nie każdy to robi.

5. Zysk na dobrym łączu jest minimalny

Mówmy wprost: jeśli Twoi użytkownicy siedzą na światłowodzie 10 km od serwera z 0% packet loss - HTTP/3 nie da im nic zauważalnego. Zysk z QUIC rośnie z odległością i jakością łącza. Lokalni użytkownicy na dobrym łączu mogą mieć nawet marginalne pogorszenie (QUIC na CPU jest droższy niż TCP, bo szyfrowanie jest w userspace, nie w kernelu).

Kiedy warto, kiedy nie

Warto włączyć HTTP/3 gdy:

  • Masz użytkowników rozproszonych geograficznie (SaaS, e-commerce)
  • Twoja aplikacja jest real-time (LiveView, chat, dashboardy)
  • Użytkownicy korzystają z mobilnych sieci (LTE, WiFi w terenie)
  • Masz dużo zasobów statycznych do pobrania (JS, CSS, obrazki)
  • Używasz CDN (włączenie HTTP/3 to jedno kliknięcie)

Nie priorytetyzuj HTTP/3 gdy:

  • Twoi użytkownicy to wewnętrzna sieć firmowa z niskim latency
  • Aplikacja działa w jednym data center (backend-to-backend)
  • Masz korporacyjne firewalle blokujące UDP (HTTP/3 i tak spadnie na HTTP/2)
  • Twój stos nie ma debugowania QUIC - i potrzebujesz widzieć ruch sieciowy

Co dalej - WebTransport i koniec ery WebSocketów?

WebSocket ma 15 lat (RFC 6455, 2011). Działał znakomicie - ale ma ograniczenia, które QUIC rozwiązuje. WebTransport to jego naturalny następca:

RokWydarzenie
2011WebSocket (RFC 6455) - standard
2015HTTP/2 (RFC 7540) - multiplexing, ale na TCP
2021HTTP/3 (RFC 9114) - QUIC zamiast TCP
2022WebSocket over HTTP/2 (RFC 9220) - ale brak implementacji w przeglądarkach
2022-2025WebTransport draft - nowy standard nad QUIC
2026-2027WebTransport stabilny? Chrome i Edge gotowe, czekamy na Firefox i Safari
2028+WebTransport mainstream?

Czy WebSocket umrze? Nie w najbliższych 5 latach. Ma 99%+ wsparcia w przeglądarkach, dojrzały ekosystem i miliony działających aplikacji. Ale nowe projekty zaczną wybierać WebTransport - tak jak nowe projekty wybierają HTTP/3 zamiast HTTP/1.1.

Dla Phoenix LiveView to oznacza: kiedy Bandit dostanie QUIC, a WebSock dostanie WebTransport - LiveView automatycznie zyska:

  • Brak head-of-line blocking na aktualizacjach DOM
  • Migrację połączeń (WiFi → LTE bez zerwania sesji)
  • Tryb unreliable dla danych, które nie wymagają gwarancji dostarczenia
  • 0-RTT reconnect po zerwaniu połączenia

I to wszystko bez zmian w kodzie aplikacji. Bo architektura Phoenixa jest na to gotowa.


Chcesz przyspieszyć swoją aplikację webową bez przepisywania kodu? Porozmawiajmy - pomożemy skonfigurować HTTP/3 przez CDN i pokażemy, ile milisekund tracisz na każdym żądaniu.