Partial Prerendering: jedna strona, kilka momentów renderowania
Przez długi czas wybór strategii renderowania przypisywaliśmy całemu route'owi.
Strona była:
statyczna
renderowana na serwerze
albo renderowana po stronie klienta
Problem w tym, że realne strony bardzo rzadko są jednorodne.
Product detail page może mieć statyczny opis produktu, dynamiczną cenę, koszyk zależny od sesji i rekomendacje, które pojawiają się później.
Dlaczego więc cała strona miałaby mieć jeden lifecycle?
Route to za duża jednostka decyzji
Załóżmy:
/products/42
Na tej stronie mamy:
header -> prawie zawsze taki sam
product description -> zmienia się rzadko
price -> zmienia się często
cart -> zależy od użytkownika
recommendations -> wolny backend
footer -> praktycznie statyczny
Jeżeli traktujemy route jako jedną jednostkę, mamy kiepski wybór.
Całość jako SSR:
request
|
v
render everything
|
v
send
oznacza, że statyczne fragmenty są liczone przy każdym requestcie.
Całość jako SSG:
build
|
v
render everything
oznacza problem z ceną i koszykiem.
Naturalna odpowiedź brzmi:
renderuj różne fragmenty w różnym czasie.
Statyczny shell, dynamiczne dziury
Mental model Partial Prerenderingu można uprościć do:
┌──────────────────────────────┐
│ STATIC │
│ header │
│ product description │
│ │
│ ┌──────────────────────┐ │
│ │ DYNAMIC PRICE │ │
│ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ DYNAMIC CART │ │
│ └──────────────────────┘ │
│ │
│ footer │
└──────────────────────────────┘
Statyczna część może powstać wcześniej i być agresywnie cache'owana.
Dynamiczne fragmenty są uzupełniane później, na przykład podczas requestu albo przez stream.
To nie jest już:
static page
ani:
dynamic page
Tylko kompozycja obu modeli.
Komercyjny przykład: e-commerce
Sklep jest tutaj idealnym przypadkiem.
Opis produktu, zdjęcia czy specyfikacja techniczna mogą zmieniać się rzadko.
Cena może zależeć od promocji.
Koszyk zależy od sesji użytkownika.
Dostępność może zmieniać się bardzo często.
Rekomendacje mogą pochodzić z wolnego systemu ML.
Przypisanie wszystkim tym elementom jednego TTL albo jednego momentu renderowania jest sztuczne.
Lepszy model:
description -> prerender
price -> request-time
cart -> user-specific
recs -> streamed later
Współczesne meta-frameworki coraz częściej pozwalają modelować stronę właśnie w taki sposób.
Cache boundary staje się ważniejszy niż route boundary
To prowadzi do interesującej zmiany mental modelu.
W klasycznym SSG pytaliśmy:
czy ta strona może być statyczna?
Teraz pytanie brzmi:
która część tej strony może być statyczna?
To dużo bardziej precyzyjne.
Możemy mieć fragment, który jest bezpieczny do współdzielenia przez milion użytkowników, obok fragmentu, który istnieje tylko dla jednej sesji.
shared cache
|
v
static shell
request-specific compute
|
v
personalized fragment
Dzięki temu personalization nie musi automatycznie niszczyć cache'owalności całego dokumentu.
Ciekawostka
Partial Prerendering można potraktować jak renderowanie z placeholderami.
Wyobraźmy sobie, że build tworzy:
<header>...</header>
<section class="product">
...
</section>
<!-- PRICE_SLOT -->
<!-- CART_SLOT -->
<footer>...</footer>
A później request-time runtime wypełnia brakujące fragmenty.
Realne implementacje są oczywiście bardziej złożone, ale mental model jest użyteczny:
precomputed structure
+
late-bound values
Podobne idee istnieją od dawna w systemach templatingowych, edge-side includes czy fragment caching.
Nowością nie jest więc sama idea „dziur”, tylko coraz lepsza integracja tego modelu z komponentami, streamingiem i client runtime'em.
Granice muszą być dobrze dobrane
Partial rendering brzmi świetnie, ale granularity ma koszt.
Jeśli każda mała część UI staje się osobną dynamiczną granicą:
fragment
fragment
fragment
fragment
fragment
fragment
system musi zarządzać większą liczbą:
zależności
fetchy
cache entries
streamowanych fragmentów
fallbacków
failure boundaries
Zbyt duża granularność może stworzyć więcej overheadu niż oszczędności.
Dlatego dobre granice zwykle odpowiadają realnym różnicom w:
freshness
personalization
latency
cacheability
Nie po prostu strukturze komponentów.
Komponent nie zawsze jest dobrą granicą renderowania
To ważne dla frontendowców.
Możemy mieć komponent:
<ProductCard />
ale w środku:
name -> static
image -> static
price -> dynamic
availability -> dynamic
favorite -> user-specific
Granica komponentu jest organizacyjna.
Granica cache czy renderowania jest architektoniczna.
To dwie różne rzeczy.
Framework może je połączyć dla wygody, ale senior frontend engineer powinien rozumieć, że nie są tożsamościowe.
Partial Prerendering dobrze łączy się ze streamingiem
Statyczny shell może być dostępny natychmiast.
Dynamiczne fragmenty mogą być generowane równolegle:
static shell
|
v
SEND
price ------------------> SEND
cart -------------------> SEND
recommendations --------------------> SEND
To daje bardzo ciekawy model:
część outputu powstała przed requestem
część powstaje podczas requestu
część dociera później przez stream
Jedna strona ma więc kilka różnych „zegarków”.
Ale gdzie wykonywać dynamiczne fragmenty?
Skoro część renderowania odbywa się dopiero na request path, pojawia się następne pytanie:
user in Warsaw
|
v
where should compute happen?
Na originie w USA?
W regionie europejskim?
Na edge node blisko użytkownika?
To kolejna niezależna oś.
Partial Prerendering odpowiada na pytanie:
która część strony powstaje kiedy?
Nie odpowiada na:
gdzie fizycznie wykonać tę pracę?
I właśnie tutaj dochodzimy do Edge Renderingu.