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.
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:
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:
PHP
ASP / ASP.NET
JSP
Ruby on Rails
Django
później różnego rodzaju template engines
Mechanizm pozostawał podobny.
$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:
<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:
<a href="/products?page=2">Next</a>
nie oznaczało zmiany lokalnego stanu routera.
Oznaczało:
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:
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:
/products/mechanical-keyboard
Serwer może podczas jednego requestu:
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:
click "+"
|
v
POST /cart
|
v
server
|
v
database
|
v
render entire page
|
v
HTTP response
|
v
parse entire document
|
v
paint
Żeby zmienić:
Quantity: 1
na:
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:
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ć.