Konrad Kowalski (rootsher)Principal Platform & Reliability Architect111111111001011001101111110100011000001110001111

Connections: dlaczego jeden fetch() nie oznacza jednego połączenia

data
kategoria
Frontend
także w
Networking
czytanie
1 min / 276 słów

W kodzie:

js
await fetch("/api/a");
await fetch("/api/b");

widzimy dwa requesty.

Nie oznacza to:

text
two requests
=
two TCP connections

HTTP request i transport connection mają różny lifecycle.

Connection setup kosztuje

Dla klasycznego HTTPS over TCP ścieżka obejmuje co najmniej:

text
DNS
  |
  v
TCP connection
  |
  v
TLS
  |
  v
HTTP

Jeżeli każde fetch() tworzyłoby nową relację od zera, latency setupu dominowałoby przy małych requestach.

Dlatego browser reuse'uje connections.

HTTP/1.1 persistent connections

W nowoczesnym HTTP/1.1 połączenie jest domyślnie persistent, chyba że strony uzgodnią jego zamknięcie.

Czyli:

text
TCP connection
├── request A -> response A
├── request B -> response B
└── request C -> response C

To usuwa koszt zestawiania TCP/TLS dla każdego requestu.

Ale HTTP/1.1 ma ograniczenie: współbieżność na jednym połączeniu jest problematyczna.

To doprowadziło browsery do utrzymywania kilku connections do originu.

fetch() nie kontroluje socket poola

Frontend nie robi:

js
fetch(url, {
  tcpConnection: 3
});

Browser zarządza connection pool.

To on decyduje:

  • czy reuse'ować istniejące connection,
  • czy zestawić nowe,
  • jaki protokół został wynegocjowany,
  • czy request może współdzielić connection z innymi requestami.

Frontend wpływa na to pośrednio przez architekturę zasobów i originów.

Origin ma znaczenie

Requesty do:

text
https://app.example.com
https://api.example.com
https://cdn.example.com

nie są automatycznie jednym logicznym connection poolem tylko dlatego, że należą do tej samej firmy.

Historycznie frontend próbował zwiększać równoległość przez rozbijanie assetów na wiele hostname'ów:

text
img1.example.com
img2.example.com
img3.example.com

To nazywano domain sharding.

Było odpowiedzią na ograniczenia HTTP/1.1.

W HTTP/2 często staje się kontrproduktywne, bo rozbija ruch, który mógłby korzystać ze wspólnego multiplexed connection.

Keep-alive w fetch() to inny temat

Fetch ma opcję:

js
fetch("/analytics", {
  method: "POST",
  body: data,
  keepalive: true
});

Nazwa może mylić.

To nie jest przełącznik HTTP persistent connection.

Fetch keepalive dotyczy możliwości kontynuowania określonego requestu po rozpoczęciu unload strony, np. dla małej telemetrii.

Nie należy mieszać:

text
Fetch keepalive option

z:

text
HTTP connection persistence

Frontendowy problem HTTP/1.1

Mamy więc persistent connections, ale pojedyncze połączenie nie daje nam jeszcze wygodnej współbieżności wielu niezależnych response'ów.

A typowa strona potrzebuje:

text
HTML
CSS
JS
fonts
images
API calls

To właśnie ograniczenia tego modelu stworzyły charakterystyczny network waterfall HTTP/1.1.

I przez lata wpływały na sposób, w jaki budowaliśmy frontend bundles.