Konrad Kowalski (rootsher)Principal Platform & Reliability Architect000101100011001000011000001000000110001101010100

Powrót SSR: serwer znowu renderuje HTML

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

CSR dał frontendowi pełną kontrolę nad aplikacją, ale pierwsze załadowanie zaczęło mieć coraz dłuższy critical path.

Browser musiał pobrać JavaScript, uruchomić runtime, odkryć zależności danych i dopiero potem wygenerować UI.

Naturalna odpowiedź była prosta:

wyrenderuj pierwszy widok wcześniej, na serwerze.

SSR wraca, ale w innej formie

Klasyczny server-rendered web działał tak:

text
request
  |
  v
server
  |
  v
HTML
  |
  v
browser

Nowoczesny SSR dodaje jeszcze jeden etap:

text
request
  |
  v
server render
  |
  v
HTML
  |
  v
browser shows content
  |
  v
JavaScript loads
  |
  v
application takes over

To ważna różnica.

Serwer nie renderuje strony po to, żeby frontend przestał istnieć.

Renderuje ją po to, żeby pierwszy widok powstał wcześniej.

Potem browser nadal może działać jak SPA.

Po co robić tę samą pracę na serwerze

Załóżmy, że użytkownik otwiera:

text
/products/42

W czystym CSR może być tak:

text
HTML shell
  |
  v
JS
  |
  v
router
  |
  v
fetch product
  |
  v
render

W SSR:

text
request /products/42
  |
  v
server fetches product
  |
  v
server renders HTML
  |
  v
browser receives content

Użytkownik może zobaczyć nazwę, cenę i opis produktu zanim cały runtime aplikacji zdąży się uruchomić.

To skraca ścieżkę do pierwszej treści.

Ale nie oznacza, że koszt klienta znika.

SSR nie usuwa JavaScriptu

To jeden z najważniejszych punktów.

Jeśli aplikacja ma być później interaktywna, browser nadal często musi pobrać:

text
framework
komponenty
routing
event handling
application state
kod odpowiedzialny za kolejne interakcje

Dlatego SSR nie jest:

text
server zamiast browsera

Tylko raczej:

text
server najpierw
browser później

W praktyce część pracy może zostać wykonana dwukrotnie.

Serwer generuje HTML, a klient później rekonstruuje komponentowy runtime potrzebny do dalszego działania aplikacji.

Ten proces stanie się osobnym problemem, który później nazwiemy hydration.

TTFB zaczyna znowu mieć znaczenie

W CSR serwer może zwrócić niemal natychmiast:

html
<div id="app"></div>

To daje świetny TTFB, ale niewiele mówi o tym, kiedy pojawi się właściwy content.

SSR przesuwa pracę przed odpowiedź.

text
request
  |
  v
database
  |
  v
business logic
  |
  v
render
  |
  v
response

To może zwiększyć Time to First Byte.

I tu pojawia się pierwszy realny trade-off:

text
CSR
-> szybki pusty response
-> więcej pracy później

SSR
-> więcej pracy przed response
-> więcej gotowej treści wcześniej

Dlatego stwierdzenie:

SSR jest szybszy

jest zbyt proste.

Szybszy w którym miejscu ścieżki?

Komercyjny przykład: product detail page

Sklep internetowy jest dobrym przypadkiem dla SSR.

Użytkownik wchodzi bezpośrednio z wyszukiwarki na stronę produktu.

Najważniejsze dane są znane już podczas requestu:

text
nazwa
cena
zdjęcie
dostępność
opis

Serwer może pobrać je i od razu wygenerować:

html
<h1>Mechanical Keyboard</h1>
<p>129 zł</p>
<p>In stock</p>

Browser nie musi czekać, aż framework wykona fetch i pierwszy render.

Jednocześnie później można uruchomić interaktywny koszyk, warianty produktu czy rekomendacje.

Współczesne frameworki takie jak Next.js, Nuxt czy SvelteKit wykorzystują ten model jako jedną z dostępnych strategii.

Ciekawostka

SSR może stać się problemem CPU nawet wtedy, gdy pojedynczy render jest szybki.

Załóżmy:

text
render = 20 ms CPU

Przy jednym użytkowniku to praktycznie nic.

Przy tysiącach requestów na sekundę sytuacja wygląda inaczej.

Jeżeli wszystkie requesty wymagają renderowania:

text
request
request
request
request
request
  |
  v
limited CPU cores

zaczyna pojawiać się kolejka.

Latency użytkownika nie jest wtedy tylko:

text
render time

ale:

text
queue time + render time

Dlatego przy dużym ruchu optymalizacja SSR staje się problemem nie tylko frameworka, ale też concurrency, cache i capacity planning.

Personalizacja komplikuje cache

SSR świetnie pasuje do dynamicznych danych.

Ale im bardziej odpowiedź zależy od requestu, tym trudniej ją cache'ować.

Przykład:

text
/products/42

Jeśli każdy użytkownik widzi ten sam HTML, odpowiedź można łatwo trzymać na CDN.

Ale jeśli strona zależy od:

text
user
country
currency
experiment
session
pricing tier

liczba możliwych wariantów rośnie.

W praktyce trzeba zdecydować, co naprawdę musi być dynamiczne.

Często tylko mały fragment strony jest spersonalizowany, podczas gdy reszta mogłaby być współdzielona przez wszystkich użytkowników.

To pytanie później doprowadzi do bardziej granularnych modeli renderowania.

SSR może też tworzyć waterfall

Przeniesienie renderu na serwer nie usuwa zależności danych.

Może jedynie przenieść je w inne miejsce.

Źle napisany SSR:

text
fetch user
  |
  v
fetch product
  |
  v
fetch reviews
  |
  v
fetch recommendations
  |
  v
render

oznacza, że użytkownik czeka na cały sekwencyjny chain zanim dostanie pierwszy bajt HTML.

Serwer może więc stworzyć dokładnie taki sam waterfall, jaki wcześniej mieliśmy w browserze.

Tyle że teraz waterfall blokuje całą odpowiedź.

SSR rozwiązuje jeden problem i tworzy następny

Powrót renderowania na serwer poprawił pierwsze wyświetlenie treści, ale pojawiło się pytanie:

co dalej?

Mamy gotowy HTML.

Ale browser nadal potrzebuje JavaScriptu, żeby buttony działały, state był aktualny, a navigation pozostała client-side.

Czyli trzeba połączyć:

text
HTML generated on server

z:

text
application runtime in browser

To połączenie nie jest darmowe ani trywialne.

Właśnie dlatego kolejnym dużym elementem współczesnej architektury renderowania stała się hydration.