Konrad Kowalski (rootsher)Principal Platform & Reliability Architect011110111101100100111111011101111000111010001010

Partial Prerendering: jedna strona, kilka momentów renderowania

data
kategoria
Frontend
także w
Caching
czytanie
3 min / 525 słów

Przez długi czas wybór strategii renderowania przypisywaliśmy całemu route'owi.

Strona była:

text
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:

text
/products/42

Na tej stronie mamy:

text
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:

text
request
  |
  v
render everything
  |
  v
send

oznacza, że statyczne fragmenty są liczone przy każdym requestcie.

Całość jako SSG:

text
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:

text
┌──────────────────────────────┐
│ 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ż:

text
static page

ani:

text
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:

text
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.

text
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:

html
<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:

text
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ą:

text
fragment
fragment
fragment
fragment
fragment
fragment

system musi zarządzać większą liczbą:

text
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:

text
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:

text
<ProductCard />

ale w środku:

text
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:

text
static shell
  |
  v
SEND

price ------------------> SEND
cart -------------------> SEND
recommendations --------------------> SEND

To daje bardzo ciekawy model:

text
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:

text
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.