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 serweraPięć 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:
| Etap | HTTP/2 (TCP + TLS 1.3) | HTTP/3 (QUIC) |
|---|---|---|
| Transport handshake | 1 RTT (TCP SYN/ACK) | 0 RTT (w QUIC) |
| Crypto handshake | 1 RTT (TLS 1.3) | Razem z transportem |
| Pierwsze dane | Po 2 RTT | Po 1 RTT |
| Połączenie wznowione (0-RTT) | 1 RTT (TCP) + 0 RTT (TLS) = 1 RTT | 0 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?
| Trasa | RTT | Oszczędność 1 RTT | Oszczędność 2 RTT (0-RTT) |
|---|---|---|---|
| Warszawa → Frankfurt | ~20 ms | 20 ms | 40 ms |
| Warszawa → Londyn | ~30 ms | 30 ms | 60 ms |
| Warszawa → Nowy Jork | ~90 ms | 90 ms | 180 ms |
| Warszawa → Singapur | ~180 ms | 180 ms | 360 ms |
| Warszawa → Sydney | ~300 ms | 300 ms | 600 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)
| Lokalizacja | HTTP/2 | HTTP/3 | Różnica |
|---|---|---|---|
| Nowy Jork (bliski serwer) | ~800 ms | ~600 ms | 200 ms szybciej |
| Londyn | ~1400 ms | ~800 ms | 600 ms szybciej |
| Bangalore | ~2200 ms | ~1600 ms | 600 ms szybciej |
Średnia strona (105 zasobów, 10 MB)
| Lokalizacja | HTTP/2 | HTTP/3 | Różnica |
|---|---|---|---|
| Nowy Jork | ~2800 ms | ~2475 ms | 325 ms szybciej |
| Londyn | ~5200 ms | ~4000 ms | 1200 ms szybciej |
| Bangalore | ~8500 ms | ~6500 ms | 2000 ms szybciej |
SPA (115 zasobów, 15 MB)
| Lokalizacja | HTTP/2 | HTTP/3 | Różnica |
|---|---|---|---|
| Nowy Jork | ~3500 ms | ~3200 ms | 300 ms szybciej |
| Londyn | ~6000 ms | ~5000 ms | 1000 ms szybciej |
| Bangalore | ~10000 ms | ~7500 ms | 2500 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 loss | HTTP/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:
| Cecha | WebSocket | WebTransport |
|---|---|---|
| Transport | TCP | QUIC (UDP) |
| Head-of-line blocking | Tak - jeden zgubiony pakiet blokuje wszystko | Nie - strumienie są niezależne |
| Strumienie | 1 dwukierunkowy | Wiele niezależnych |
| Tryb unreliable | Brak | Datagramy - dane bez gwarancji dostarczenia |
| Migracja sieci | Nie - zmiana IP = zerwane połączenie | Tak - przeżywa zmianę WiFi → LTE |
| 0-RTT | Nie | Tak |
| Wsparcie przeglądarek | 99%+ | ~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.
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ądarka | HTTP/3 | WebTransport |
|---|---|---|
| Chrome | Od wersji 87 (2020) | Od wersji 97 (2022) |
| Edge | Od wersji 87 (2020) | Od wersji 97 (2022) |
| Firefox | Od wersji 88 (2021) | Za flagą |
| Safari | Od wersji 16 (2022) | Brak |
100% głównych przeglądarek obsługuje HTTP/3. WebTransport - na razie Chrome i Edge.
Serwery - tu jest problem
| Serwer | HTTP/3 |
|---|---|
| Caddy | Pełne wsparcie (domyślnie włączone) |
| Nginx | Eksperymentalne (wyłączone domyślnie) |
| Apache | Brak 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ługa | HTTP/3 |
|---|---|
| Cloudflare | Tak (domyślnie) |
| AWS CloudFront | Tak |
| Google Cloud CDN | Tak |
| Fastly | Tak |
| Akamai | Tak |
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.
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
| Metryka | Wartość (Q1 2026) |
|---|---|
| Strony obsługujące HTTP/3 | ~31-36% (w3techs) |
| Ruch przez QUIC (Cloudflare) | ~35% globalnego ruchu webowego |
| Wsparcie przeglądarek | 100% głównych |
| Udział mobilny | HTTP/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.
- Wrzuć aplikację za Cloudflare (lub inny CDN z HTTP/3)
- Włącz HTTP/3 w panelu CDN (w Cloudflare jest domyślnie włączone)
- 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=86400Nagłó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-RTTOpcja 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:
| Rok | Wydarzenie |
|---|---|
| 2011 | WebSocket (RFC 6455) - standard |
| 2015 | HTTP/2 (RFC 7540) - multiplexing, ale na TCP |
| 2021 | HTTP/3 (RFC 9114) - QUIC zamiast TCP |
| 2022 | WebSocket over HTTP/2 (RFC 9220) - ale brak implementacji w przeglądarkach |
| 2022-2025 | WebTransport draft - nowy standard nad QUIC |
| 2026-2027 | WebTransport 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.