SSG: renderowanie zanim pojawi się użytkownik
- data
- kategoria
- Frontend
- czytanie
- 3 min / 568 słów
SSR skraca drogę do pierwszej treści, ale ma koszt: renderowanie odbywa się na ścieżce requestu.
Jeśli użytkownik otwiera stronę produktu, serwer może pobrać dane, zbudować drzewo komponentów i wygenerować HTML. To działa dobrze, dopóki naprawdę potrzebujemy robić tę pracę przy każdym wejściu.
Ale co jeśli treść zmienia się raz dziennie?
Wtedy renderowanie per request zaczyna wyglądać jak marnowanie CPU.
Przenieśmy render jeszcze wcześniej
SSR wykonuje:
request
|
v
render
|
v
HTML
SSG przesuwa ten sam render do build-time:
build
|
v
render
|
v
HTML artifact
|
v
deploy
|
v
request
|
v
serve file
Dla użytkownika różnica jest ogromna.
Serwer nie musi już uruchamiać logiki renderującej dla każdego requestu. Gotowy dokument może leżeć na CDN i zostać zwrócony praktycznie jak zwykły plik.
To właśnie Static Site Generation.
SSG to precomputation
Najprostszy mental model:
HTML = render(data)
Jeżeli data jest znane wcześniej, wynik tej funkcji też możemy policzyć wcześniej.
Zamiast robić:
for every request:
render(product)
robimy:
during build:
render(product)
save result
Jeśli mamy 10 000 produktów:
/products/1.html
/products/2.html
/products/3.html
...
powstaje zestaw gotowych artefaktów.
Request użytkownika nie uruchamia już renderera. Dostaje wcześniej przygotowany wynik.
Komercyjny przykład: dokumentacja
Dokumentacja techniczna jest niemal idealnym przypadkiem SSG.
Treść:
zmienia się stosunkowo rzadko
jest publiczna
powinna być szybko dostępna globalnie
nie zależy zwykle od konkretnego użytkownika
Po każdym merge'u można wykonać:
Markdown
|
v
build
|
v
HTML
|
v
CDN
Użytkownik później dostaje gotowy dokument.
Dlatego systemy dokumentacyjne, blogi, landing pages czy strony marketingowe bardzo dobrze pasują do statycznego generowania.
Frameworki takie jak Astro, Next.js czy Nuxt potrafią wykorzystywać ten model, ale sama idea jest dużo starsza od współczesnych meta-frameworków.
Dlaczego to jest tak szybkie
Request-time path staje się bardzo krótki:
browser
|
v
CDN
|
v
cached HTML
Nie ma:
query do bazy
uruchamiania aplikacji
renderowania komponentów
template engine
server-side data fetching
Co ważniejsze, gotowy HTML można kopiować do wielu lokalizacji geograficznych.
Nie musimy więc pytać centralnego originu o każdą stronę.
To daje bardzo dobry profil:
low latency
+
high cacheability
+
almost zero render CPU per request
Brzmi idealnie.
Problem zaczyna się wtedy, gdy spojrzymy na koszt builda.
Renderowanie nie zniknęło
SSG nie eliminuje pracy.
Przenosi ją w czasie.
Załóżmy:
100 pages
x
50 ms render
=
5 seconds
Nie ma problemu.
Teraz:
100 000 pages
x
50 ms
=
5 000 seconds
To ponad godzina samego renderowania przy naiwnym wykonaniu sekwencyjnym.
Oczywiście realne systemy równoleglą pracę, cache'ują rezultaty i wykonują buildy inkrementalne, ale fundamentalny problem pozostaje:
koszt rośnie wraz z liczbą materializowanych wariantów.
Ciekawostka
SSG można potraktować jak materialized view z baz danych.
Mamy funkcję:
page = f(content, configuration, code)
Zamiast obliczać f() przy każdym odczycie, przechowujemy jej wynik.
To daje bardzo szybki read, ale tworzy klasyczny problem systemów z precomputed data:
kiedy wynik przestaje być aktualny?
Dokładnie ten sam trade-off występuje w cache'ach, indeksach czy materialized views:
cheaper reads
<->
more expensive updates
SSG jest więc mniej „techniką frontendową”, a bardziej konkretnym przypadkiem precomputation.
Freshness staje się problemem
Załóżmy, że wygenerowaliśmy:
<p>Price: 129 zł</p>
O 12:00 cena zmienia się na:
119 zł
Statyczny artefakt sam się nie zmieni.
Trzeba:
data change
|
v
trigger build
|
v
render page again
|
v
deploy new artifact
|
v
invalidate / replace cache
Dla dokumentacji jest to zwykle akceptowalne.
Dla ceny produktu, availability albo wyników meczu już znacznie mniej.
Dlatego SSG działa najlepiej wtedy, gdy tolerancja na stale content jest zgodna z częstotliwością rebuildów.
Problem kombinacji
Jeszcze ciekawiej robi się przy stronach generowanych z wielu parametrów.
Załóżmy marketplace:
category
x
country
x
language
x
page
x
filters
Jeśli próbujemy materializować wszystkie kombinacje:
100 categories
x 20 countries
x 10 languages
x 100 pages
=
2 000 000 variants
A to jeszcze przed filtrami.
Nie każda możliwa strona zostanie kiedykolwiek odwiedzona.
Generowanie wszystkiego podczas builda oznacza więc wykonywanie dużej ilości pracy dla hipotetycznych requestów.
SSG zmienia ekonomię requestu
W SSR płacimy za render wtedy, kiedy użytkownik przychodzi:
request -> compute
W SSG płacimy wcześniej:
build -> compute -> store
Późniejszy request jest bardzo tani.
Można więc patrzeć na wybór między SSR i SSG jak na decyzję:
kiedy chcemy zapłacić koszt renderowania?
przed ruchem
czy podczas ruchu?
Jeżeli strona będzie odwiedzana milion razy, jeden wcześniejszy render może być świetną inwestycją.
Jeśli strona nigdy nie zostanie odwiedzona, ten sam render był całkowicie zbędny.
I właśnie tutaj SSG wpada na swój naturalny limit.
Nie chcemy renderować wszystkiego przy każdym requeście.
Ale nie chcemy też generować podczas builda milionów stron, których być może nikt nie otworzy.
Potrzebujemy trzeciej opcji:
wygeneruj stronę dopiero wtedy, gdy ktoś naprawdę jej potrzebuje, a potem zachowaj wynik.
Tak dochodzimy do deferred generation.