Server Components: nie cały kod UI musi trafić do browsera
- data
- kategoria
- Frontend
- także w
- Software Architecture
- czytanie
- 3 min / 646 słów
Przez większość tej historii pytaliśmy:
kiedy renderować?
Potem:
gdzie renderować?
Hydration, islands i resumability dokładają kolejne pytanie:
kiedy uruchomić kod po stronie klienta?
Server Components zmieniają poziom abstrakcji.
Pytanie brzmi:
czy ten kod w ogóle powinien trafić do browsera?
Component tree nie musi oznaczać jednego runtime'u
Klasyczny model komponentowy zakłada zwykle, że komponent jest częścią aplikacji, którą ostatecznie potrafi wykonać klient.
Server Components pozwalają przeciąć drzewo:
App
├── ProductPage -> server
│ ├── ProductDetails -> server
│ ├── Recommendations -> server
│ └── AddToCart -> client
└── Navigation -> client
Część komponentów istnieje tylko po stronie serwera.
Nie trzeba wysyłać ich implementacji do browsera.
To istotna różnica względem zwykłego SSR.
SSR i Server Components rozwiązują inne problemy
SSR odpowiada na pytanie:
gdzie powstaje HTML dla pierwszego renderu?
Server Components odpowiadają:
w którym runtime może wykonywać się dany komponent?
Możemy mieć komponent wyrenderowany na serwerze podczas SSR, którego kod później i tak trafia do browsera na potrzeby hydration.
W Server Components część kodu może być całkowicie server-only.
SSR:
component code
|
v
server render
|
v
HTML
|
v
same component code ships to client
Server Component:
component code
|
v
server execution
|
v
serialized result
|
v
no client implementation required
To dlatego utożsamianie RSC z SSR jest błędem.
Dlaczego w ogóle wysyłamy tyle kodu
Załóżmy komponent, którego jedynym zadaniem jest pobranie produktu i przygotowanie danych:
async function ProductDetails({ id }) {
const product = await db.products.find(id);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
</section>
);
}
Jeżeli ten komponent nie potrzebuje:
lokalnego state
event handlerów
browser APIs
efektów
to jego implementacja jest potrzebna głównie po to, żeby stworzyć wynik renderowania.
Po co więc wysyłać ją użytkownikowi?
Server Components pozwalają zatrzymać tę część grafu po stronie serwera.
Komercyjny przykład: product page
Załóżmy dużą stronę produktu.
Mamy:
ProductPage
├── ProductDescription
├── TechnicalSpecs
├── RelatedProducts
├── ReviewsSummary
├── VariantSelector
└── AddToCart
Pierwsze cztery fragmenty mogą nie potrzebować client runtime'u.
VariantSelector i AddToCart są interaktywne.
Możemy więc podzielić system:
SERVER
├── ProductDescription
├── TechnicalSpecs
├── RelatedProducts
└── ReviewsSummary
CLIENT
├── VariantSelector
└── AddToCart
Dzięki temu browser dostaje kod odpowiedzialny za interakcję, a nie cały kod potrzebny do skonstruowania strony.
Granica server/client jest granicą dependency graphu
To najważniejszy techniczny detal.
Wyobraźmy sobie:
ProductPage
|
v
MarkdownRenderer
|
v
syntax-highlighting-library
|
v
large parser
Jeżeli cały ten graph należy do client bundle, browser może dostać dużo kodu tylko po to, żeby wyrenderować treść raz.
Jeśli ProductPage jest server-only:
server graph
├── MarkdownRenderer
├── parser
└── database client
te dependencies nie muszą przekraczać granicy sieciowej.
Client dostaje tylko wynik.
Dlatego Server Components można traktować jako mechanizm kontroli dystrybucji kodu, a nie tylko renderowania.
Ciekawostka
Granica Server Component -> Client Component wymaga serializacji.
Server Component nie może po prostu przekazać dowolnej wartości do klienta.
Jeżeli spróbujemy przekazać coś w rodzaju:
{
connection: databaseConnection,
handler: () => doSomething(),
stream: internalServerObject
}
client runtime nie ma jak odtworzyć tych obiektów.
Granica sieciowa wymusza kontrakt danych.
W praktyce oznacza to, że architektura komponentów zaczyna mieć podobne ograniczenia jak projektowanie API:
server value
|
v
serializable representation
|
v
network boundary
|
v
client value
To bardzo ciekawa zmiana, bo boundary w drzewie UI staje się jednocześnie boundary systemu rozproszonego.
To może usunąć client-side waterfall
Server Components mogą też zmienić sposób pobierania danych.
W klasycznym CSR:
browser
|
v
component A loads
|
v
fetch A
|
v
component B discovered
|
v
fetch B
Powstaje waterfall.
Jeżeli server component zna cały potrzebny graph wcześniej:
server
├── fetch A
├── fetch B
└── render result
część pracy można wykonać bliżej danych i równolegle.
To nie oznacza automatycznie braku waterfalli: źle zaprojektowany server component nadal może robić sekwencyjne fetche.
Ale zmienia się miejsce, w którym można dependency graph zoptymalizować.
Server Components nie eliminują client components
Jeżeli komponent potrzebuje:
useState()
albo:
window.addEventListener(...)
albo event handlera:
<button onClick={...}>
to nadal potrzebujemy kodu po stronie klienta.
Dlatego realna aplikacja wygląda raczej tak:
server-heavy tree
|
v
client boundaries
|
v
interactive subtrees
Nie:
everything on server
Najważniejszą decyzją staje się więc miejsce postawienia granicy.
Zbyt wysoka granica = za dużo JavaScriptu
Jeżeli oznaczymy duży fragment jako client-side:
CLIENT
└── ProductPage
├── Description
├── Specs
├── Reviews
├── Gallery
└── AddToCart
cały dependency graph poniżej może zacząć należeć do klienta.
Jeżeli granicę przesuniemy niżej:
SERVER
└── ProductPage
├── Description
├── Specs
├── Reviews
├── Gallery
└── CLIENT AddToCart
browser dostaje znacznie mniej kodu.
To dlatego "use client" w React Server Components jest architektonicznie ważniejsze, niż wygląda.
Nie opisuje tylko komponentu.
Może wpływać na cały graf zależności poniżej boundary.
Typowe rozwiązanie: React Server Components
Najbardziej znanym przykładem są React Server Components, wykorzystywane między innymi przez współczesne frameworki Reactowe.
Implementacja wykorzystuje specjalny format transportowy pozwalający przesyłać wynik server component tree oraz referencje do client components.
Browser nie dostaje po prostu gotowego HTML-a.
Dostaje reprezentację, którą client runtime może wykorzystać do złożenia finalnego drzewa aplikacji.
To jeden z powodów, dla których Server Components są czymś innym niż klasyczny template rendering.
Granice zaczynają się nakładać
W tym miejscu nowoczesna architektura renderowania robi się ciekawa.
Jedna strona może jednocześnie wykorzystywać:
static generation
+
revalidation
+
server components
+
streaming
+
client components
+
selective hydration
+
edge cache
Te mechanizmy nie są konkurencją.
Opisują różne osie systemu.
I właśnie dlatego pytanie:
używamy SSR czy SSG?
coraz częściej jest zbyt uproszczone.
Współczesne aplikacje nie wybierają jednej strategii.
Komponują wiele strategii jednocześnie.
To będzie tematem finału tej serii.