Konrad Kowalski (rootsher)Principal Platform & Reliability Architect101001111010110100010110011100101011011110100110

Nowoczesny rendering to kompozycja

data
kategoria
Frontend
także w
Software Architecture · System Design
czytanie
3 min / 562 słów

Przeszliśmy długą drogę od prostego modelu:

text
request
  |
  v
server
  |
  v
HTML
  |
  v
browser

Potem pojawiły się AJAX, SPA, CSR, powrót SSR, SSG, deferred generation, revalidation, streaming, hydration, islands, resumability i Server Components.

Łatwo z tego zrobić zoo akronimów.

Lepszy mental model jest prostszy:

współczesna architektura renderowania to zestaw niezależnych decyzji o compute, cache, sieci i client runtime.

Nie wybierasz już jednego trybu dla aplikacji

Dawniej pytanie brzmiało:

text
SSR czy CSR?

Później:

text
SSR czy SSG?

Dzisiaj sensowniejsze pytania to:

text
kiedy renderować?
gdzie renderować?
co cache'ować?
co streamować?
co wysłać do klienta?
co hydratować?

To kilka różnych osi.

Jedna aplikacja może używać wielu odpowiedzi jednocześnie.

Weźmy realny marketplace

Załóżmy platformę podobną do dużego sklepu lub marketplace'u.

Mamy:

text
Homepage
Search
Category
Product page
Cart
Checkout
Account
Admin

Próba ustawienia jednego:

text
renderingMode = "SSR"

dla całej aplikacji jest architektonicznie mało interesująca.

Każdy fragment ma inne właściwości.

Homepage

Homepage może zawierać głównie content marketingowy:

text
hero
categories
campaign banners
popular products
footer

Duża część zmienia się stosunkowo rzadko.

Dobry model:

text
prerender
+
revalidation
+
CDN

Nie ma powodu renderować tego od zera dla każdego użytkownika.

Ale mały fragment:

text
Hello, Anna
Your cart: 3

jest personalizowany.

Nie powinien niszczyć cache'owalności całego dokumentu.

To miejsce na dynamiczny fragment albo client-side activation.

Search jest inny.

Query:

text
/search?q=mechanical+keyboard

ma potencjalnie ogromną liczbę wariantów.

Nie będziemy generować ich podczas builda.

Search może więc korzystać z:

text
request-time data
+
server render
+
client-side interactions

Pierwszy wynik może powstać na serwerze, a późniejsze:

text
filters
sorting
pagination

mogą działać po stronie klienta.

To nie jest ani „SSR application”, ani „CSR application”.

To kompozycja obu modeli.

Product detail page

Tutaj robi się jeszcze ciekawiej.

text
Product Page
├── description
├── technical specs
├── price
├── availability
├── recommendations
├── reviews
├── wishlist
└── add to cart

Każdy fragment ma inne właściwości.

Możemy mieć:

text
description
-> prerender + long cache

technical specs
-> prerender

price
-> short revalidation / dynamic

availability
-> dynamic

recommendations
-> streamed

wishlist
-> client

add to cart
-> client

Do tego część komponentów może być server-only i nigdy nie trafić do browsera.

Jedna strona wykorzystuje więc kilka generacji architektury renderowania jednocześnie.

Cart i checkout

Koszyk jest jeszcze inną klasą problemu.

Tutaj mamy:

text
silną personalizację
dużo interakcji
optimistic updates
częste mutation
bardzo świeże dane

Nie ma większego sensu próbować traktować całego cart UI jak statycznego contentu.

Client-side runtime ma tutaj dużą wartość.

Checkout może jednak nadal dostać początkowy HTML z serwera.

Czyli:

text
server initial render
+
long-lived client state

To nadal sensowny model.

Ciekawostka

Najważniejsze granice współczesnej aplikacji często nie pokrywają się z route'ami.

Możemy mieć:

text
/products/42

ale wewnątrz niego różne boundary:

text
cache boundary
streaming boundary
server/client boundary
hydration boundary
error boundary

To pięć różnych granic na tej samej stronie.

Framework może przedstawiać je wszystkie przez podobną strukturę komponentów, ale z punktu widzenia runtime'u rozwiązują inne problemy.

Dlatego component tree coraz częściej staje się jednocześnie opisem architektury wykonania.

Cztery zasoby

Większość decyzji można sprowadzić do czterech kosztów.

Compute

Gdzie wykonujemy pracę?

text
build machine
origin
edge
browser

Network

Co musi przekroczyć sieć?

text
HTML
JSON
JavaScript
component payload
images

Storage / cache

Który wynik możemy policzyć raz i później reuse'ować?

text
static artifact
CDN cache
fragment cache
data cache

Client CPU

Ile pracy zostawiamy urządzeniu użytkownika?

text
parse JS
execute JS
hydrate
render
handle interactions

Nowe techniki renderowania zwykle nie eliminują kosztu.

Przesuwają go pomiędzy tymi czterema miejscami.

SSR nie jest darmowy

Przesuwamy compute:

text
browser -> server

ale zwiększamy:

text
server CPU
request latency
infrastructure complexity

SSG nie jest darmowe

Przesuwamy compute:

text
request-time -> build-time

ale zwiększamy:

text
build cost
storage
invalidation complexity

Hydration nie jest darmowa

Kupujemy:

text
early HTML
+
client application

kosztem:

text
server render
+
client reconstruction

Islands nie są darmowe

Redukujemy:

text
global client runtime

ale zwiększamy:

text
number of boundaries
state coordination
runtime fragmentation

Każda architektura jest wymianą kosztów.

Framework powinien być konsekwencją decyzji

To odwraca popularny sposób projektowania.

Nie:

text
używamy frameworka X
  |
  v
więc będziemy używać feature Y

Tylko:

text
ten fragment:
- zmienia się rzadko
- ma duży ruch
- nie jest personalizowany
  |
  v
powinien być prerenderowany i cache'owany

A dopiero potem pytamy:

jak implementujemy to w naszym stacku?

Next.js, Nuxt, Astro, SvelteKit czy inne meta-frameworki są narzędziami do implementacji tych decyzji.

Nie powinny być ich źródłem.

Najważniejsze pytanie: gdzie postawić granice

Senior frontend engineer coraz rzadziej optymalizuje pojedyncze:

text
render()

Coraz częściej projektuje granice:

text
static | dynamic

server | client

cached | fresh

eager | lazy

blocking | streamed

Dobrze postawiona granica potrafi usunąć całe klasy kosztów.

Źle postawiona może sprawić, że:

text
personalizacja wyłączy cache całej strony
jeden client component wciągnie duży dependency graph do browsera
jeden wolny fetch zablokuje cały SSR
jedna duża hydration boundary zajmie main thread

Nie wróciliśmy do server-rendered webu

Na pierwszy rzut oka historia zatoczyła koło.

Zaczęliśmy od:

text
server -> HTML

i znowu dużo mówimy o serwerze.

Ale współczesny model jest inny.

Dzisiaj możemy decydować osobno:

text
ten fragment powstaje podczas builda
ten podczas requestu
ten na edge
ten streamujemy
ten zostaje server-only
ten aktywujemy w browserze
ten uruchamiamy dopiero po kliknięciu

Nie wróciliśmy więc do starego SSR.

Doszliśmy do architektury, w której renderowanie przestało być pojedynczym etapem.

Stało się problemem rozmieszczenia pracy w czasie, przestrzeni i pomiędzy runtime'ami.

I to jest chyba najlepszy sposób patrzenia na współczesny frontend:

nie jako wybór kolejnego skrótu,

ale jako projektowanie przepływu compute, danych i interaktywności przez cały system.