Konrad Kowalski (rootsher)Principal Platform & Reliability Architect011001010111100011110010010111010101111001000010

Server-rendered web: zanim frontend stał się aplikacją

data
kategoria
Frontend
czytanie
3 min / 580 słów

Na początku web był prostszy architektonicznie, ale niekoniecznie prymitywny.

Browser wysyłał request. Serwer wykonywał kod, pobierał dane, składał HTML i odsyłał kompletny dokument. Kliknięcie linku uruchamiało cały cykl ponownie.

text
browser
  |
  | GET /products/42
  v
server
  |
  |  query database
  |  execute application logic
  |  render HTML
  |
  v
browser
  |
  v
parse + layout + paint

Dziś nazwalibyśmy to Server-Side Renderingiem, ale wtedy nie było potrzeby dodawania SSR do nazwy. To był po prostu web.

Serwer posiadał stan widoku

Typowa aplikacja z początku lat 2000 działała według bardzo prostego modelu:

text
HTML = render(request, session, database)

Request zawierał URL, parametry formularza i cookies. Serwer na tej podstawie ustalał, jaki dokument powinien zobaczyć użytkownik.

Technologie były różne:

text
PHP
ASP / ASP.NET
JSP
Ruby on Rails
Django
później różnego rodzaju template engines

Mechanizm pozostawał podobny.

php
$product = findProduct($_GET["id"]);

render("product.php", [
    "product" => $product
]);

Browser nie musiał wiedzieć, skąd pochodzi cena produktu, czy użytkownik jest zalogowany ani jak połączyć dane z komponentem.

Dostawał wynik:

html
<h1>Mechanical Keyboard</h1>
<p>$129</p>
<button>Add to cart</button>

Granica odpowiedzialności była bardzo wyraźna.

Serwer posiadał application state, browser posiadał głównie document state.

Każda nawigacja była transakcją dokumentu

Kliknięcie:

html
<a href="/products?page=2">Next</a>

nie oznaczało zmiany lokalnego stanu routera.

Oznaczało:

text
current document
  |
  v
destroy
  |
  v
HTTP request
  |
  v
server render
  |
  v
new document

To ma ciekawą konsekwencję: wiele problemów, z którymi walczymy dzisiaj na frontendzie, praktycznie nie istniało.

Nie było potrzeby zastanawiania się nad:

text
synchronizacja client cache z backendem
cleanup komponentu działającego przez osiem godzin
hydration mismatch
bundle splitting
client-side routing
stale closure
utrzymywanie ogromnego application state w pamięci browsera

Nowy dokument oznaczał w praktyce reset dużej części środowiska.

Cena była oczywista: każda istotna interakcja wymagała round tripu do serwera i zwykle przeładowania dokumentu.

To działa zaskakująco dobrze dla wielu produktów

Wyobraźmy sobie sklep internetowy.

Użytkownik otwiera:

text
/products/mechanical-keyboard

Serwer może podczas jednego requestu:

text
1. pobrać produkt
2. sprawdzić cenę
3. sprawdzić promocję
4. odczytać sesję
5. wygenerować rekomendacje
6. wyrenderować HTML

Dla content-heavy aplikacji jest to nadal bardzo sensowny model.

HTML pojawia się bez konieczności wcześniejszego pobrania aplikacyjnego bundle'a JavaScriptu. Linki są prawdziwymi linkami. Browser wykonuje nawigację zgodnie ze swoim natywnym modelem dokumentów.

Współczesne systemy nadal korzystają z dokładnie tej idei. Next.js, Nuxt, SvelteKit czy klasyczny Rails mogą generować dokument na serwerze dla requestu.

Zmieniła się głównie skala i sposób kompozycji.

Gdzie pojawia się problem

Załóżmy teraz, że użytkownik zmienia liczbę produktów w koszyku.

W klasycznej architekturze może to wyglądać tak:

text
click "+"
  |
  v
POST /cart
  |
  v
server
  |
  v
database
  |
  v
render entire page
  |
  v
HTTP response
  |
  v
parse entire document
  |
  v
paint

Żeby zmienić:

text
Quantity: 1

na:

text
Quantity: 2

przechodzimy przez pełną nawigację.

To nie jest problem wydajności samego renderowania HTML. Problemem jest granularność interakcji.

Serwer operuje na dokumentach, podczas gdy użytkownik coraz częściej chce operować na fragmentach interfejsu.

I właśnie tutaj zaczyna się presja, która później doprowadzi do AJAX-u, a ostatecznie do SPA i CSR.

Ciekawostka

Nowy dokument nie oznacza po prostu zastąpienia document.body.

Browser uruchamia cały navigation lifecycle: pobiera odpowiedź, tworzy nowy Document, parsuje HTML, odkrywa zasoby zależne, buduje struktury potrzebne do stylowania i layoutu, a następnie przygotowuje nowe klatki do wyświetlenia.

Część CSS, JavaScriptu, fontów czy obrazów może zostać odzyskana z cache, ale sama granica dokumentu pozostaje bardzo mocną granicą lifecycle'u przeglądarki.

Współczesny SSR nie jest tym samym

Łatwo spojrzeć na współczesną aplikację Reactową renderowaną na serwerze i powiedzieć:

wróciliśmy do tego, co PHP robiło 20 lat temu.

Tylko częściowo.

Klasyczna aplikacja server-rendered często kończyła pracę po dostarczeniu dokumentu.

Współczesny SSR często robi coś innego:

text
server
  |
  v
HTML

browser
  |
  v
HTML already visible
  +
JavaScript application
  |
  v
hydration
  |
  v
long-lived client runtime

Serwer renderuje więc początkowy stan interfejsu, ale później browser przejmuje aplikację.

To fundamentalna różnica.

Klasyczny model był przede wszystkim document-oriented.

Nowoczesne frameworki są zwykle application-oriented, nawet jeśli pierwszy dokument powstaje na serwerze.

Dlaczego odeszliśmy od tego modelu

Nie dlatego, że renderowanie HTML na serwerze było wolne.

Problem był bardziej fundamentalny.

Web zaczął wymagać interakcji, dla których dokument był zbyt dużą jednostką aktualizacji.

Autocomplete nie powinien przeładowywać strony.

Zmiana sortowania tabeli nie powinna niszczyć całego dokumentu.

Chat nie może wykonywać pełnej nawigacji po każdej wiadomości.

Google Maps nie może przeładowywać strony przy każdym przesunięciu mapy.

Potrzebowaliśmy sposobu, żeby browser mógł komunikować się z serwerem bez zastępowania całego dokumentu.

Technicznie wiele elementów potrzebnych do tego istniało wcześniej. Dopiero jednak w połowie lat 2000 ten model został spopularyzowany i dostał nazwę, która została z nami na długo:

AJAX.

I od tego momentu granica między „stroną internetową” a „aplikacją działającą w browserze” zaczęła się szybko zacierać.