Revalidation i ISR: statyczny output, który może się zestarzeć
Deferred generation rozwiązuje problem pierwszego renderu.
Nie generujemy milionów stron podczas builda. Generujemy je dopiero wtedy, gdy ktoś naprawdę ich potrzebuje, a potem zachowujemy wynik.
Ale statyczny artefakt ma jedną niewygodną właściwość:
nie wie, że dane się zmieniły.
Jeśli cena produktu, stan magazynowy albo treść artykułu ulegnie zmianie, wcześniej wygenerowany HTML może stać się nieaktualny.
I tutaj pojawia się revalidation.
Cache potrzebuje pojęcia świeżości
Załóżmy, że wygenerowaliśmy stronę produktu:
<h1>Mechanical Keyboard</h1>
<p>129 zł</p>
O 12:00 cena zmienia się na 119 zł.
Jeżeli nic nie zrobimy, cache nadal zwróci:
129 zł
Musimy więc określić, kiedy wynik przestaje być fresh.
Najprostszy model:
generated at: 12:00
TTL: 60s
12:00:30 -> fresh
12:01:10 -> stale
Od tego momentu system wie, że artefakt powinien zostać odświeżony.
Pytanie brzmi tylko: kiedy?
Regeneracja synchroniczna
Najbardziej oczywista strategia:
request
|
v
cache stale?
| yes
v
render new version
|
v
store
|
v
return
To daje świeży wynik, ale pierwszy request po wygaśnięciu płaci pełny koszt renderu.
Mamy więc podobny problem jak przy deferred generation:
most users -> fast
one unlucky user -> slow
W wielu systemach można zrobić lepiej.
Stale-while-revalidate
Zamiast blokować użytkownika na regeneracji, możemy zwrócić stary wynik i odświeżyć go w tle.
request
|
v
cache stale?
| yes
v
return stale HTML
+
start regeneration
Następny request dostanie już nową wersję.
Przykład:
12:01:10
request A
-> gets 129 zł
-> triggers regeneration
12:01:11
new HTML generated: 119 zł
12:01:12
request B
-> gets 119 zł
To klasyczny trade-off:
freshness vs latency.
Akceptujemy krótkie okno nieaktualności w zamian za szybki response.
Gdzie w tym ISR
Incremental Static Regeneration można traktować właśnie jako konkretną implementację tego modelu.
Nie jako magiczną kategorię renderowania, tylko jako połączenie:
precomputed HTML
+
cache
+
expiration
+
regeneration
To istotne, bo dzięki temu ISR przestaje wyglądać jak kolejny frameworkowy skrót.
Mechanizm jest bardzo stary.
Podobne strategie istnieją od lat w CDN-ach, reverse proxy i systemach cache.
Frontend po prostu zaczął wykorzystywać je bezpośrednio do artefaktów renderowania.
Komercyjny przykład: katalog produktów
Załóżmy duży sklep.
Opis produktu zmienia się raz na kilka dni.
Cena kilka razy dziennie.
Stan magazynowy może zmieniać się co kilka sekund.
Gdyby cały dokument miał jeden TTL, musielibyśmy wybrać kompromis:
TTL 24h
-> świetny cache
-> potencjalnie bardzo stara cena
TTL 5s
-> świeża cena
-> ciągła regeneracja całej strony
To pokazuje ważny problem:
różne dane mają różne wymagania dotyczące freshness.
Nie zawsze sensowne jest traktowanie całego HTML-a jako jednej jednostki cache.
Ten problem później doprowadzi do bardziej granularnych modeli renderowania.
Ciekawostka
TTL nie oznacza:
po 60 sekundach cache automatycznie generuje nową stronę.
Najczęściej oznacza jedynie:
po 60 sekundach obecny wpis nie powinien być już traktowany jako fresh.
Regeneracja może zacząć się dopiero przy kolejnym requestcie.
Jeżeli dana strona nie dostanie żadnego ruchu przez godzinę, nie ma powodu jej aktualizować.
To kolejny przykład lazy computation:
don't refresh until somebody cares
Dzięki temu koszt systemu może skalować się bardziej z realnym ruchem niż z teoretyczną liczbą stron.
Revalidation oparta na czasie to tylko jeden model
TTL jest prosty, ale ma wadę.
Jeśli produkt zmienił się sekundę po regeneracji, możemy przez prawie cały TTL serwować stary dokument.
Alternatywą jest event-driven invalidation.
CMS update
|
v
invalidate /products/42
|
v
next request regenerates
Albo:
database update
|
v
webhook
|
v
purge cache
To pozwala utrzymywać długi TTL albo nawet brak klasycznego expiration, a jednocześnie reagować natychmiast na zmianę danych.
Koszt to większa złożoność systemu.
Musimy wiedzieć:
które artefakty zależą od zmienionych danych?
Invalidation szybko robi się problemem grafu zależności
Załóżmy, że produkt 42 występuje na:
/products/42
/category/keyboards
/search?q=keyboard
/promotions/summer
Zmiana ceny produktu może oznaczać invalidację wszystkich tych stron.
Czyli relacja nie jest już:
data -> page
ale:
data
├── page A
├── page B
├── page C
└── page D
W większej aplikacji powstaje dependency graph między danymi i cache'owanymi artefaktami.
Dlatego invalidation jest trudniejsze niż samo cache'owanie.
Cache wie, co przechowuje.
Nie zawsze wie, dlaczego wynik wygląda właśnie tak.
Co jeśli wiele requestów próbuje regenerować naraz
Załóżmy popularny produkt.
Cache właśnie wygasł.
W tej samej chwili przychodzi 500 requestów.
Naiwna implementacja:
500 requests
|
v
500 regenerations
|
v
database cries
To klasyczny cache stampede.
System powinien zwykle pozwolić jednej instancji wykonać regenerację:
request A -> regeneration
request B ─┐
request C ─┼─> stale response
request D ─┘
albo sprawić, żeby pozostałe requesty czekały na ten sam wynik.
To ten sam problem concurrency, który widzieliśmy przy deferred generation, tylko teraz pojawia się cyklicznie.
Revalidation zmienia sposób myślenia o renderowaniu
Na początku mieliśmy prosty wybór:
render now
albo:
render during build
Revalidation wprowadza trzeci model:
render
|
v
reuse
|
v
become stale
|
v
render again
Rendering staje się procesem okresowej materializacji wyniku.
I to jest dużo ciekawszy mental model niż:
ISR to SSG odświeżany co jakiś czas.
Tak naprawdę mamy system cache z kontrolowaną niespójnością.
Ale nadal czekamy na cały dokument
Nawet jeśli potrafimy świetnie cache'ować i regenerować HTML, pozostaje inne ograniczenie.
Załóżmy, że strona potrzebuje:
product -> 20 ms
reviews -> 80 ms
recommendations -> 500 ms
Jeśli serwer chce wygenerować kompletny dokument przed wysłaniem odpowiedzi, cały request może czekać na najwolniejszy fragment.
fast data ───────────┐
medium data ─────────┼─> wait -> HTML
slow data ───────────┘
A przecież użytkownik mógłby już zobaczyć nazwę i cenę produktu.
Nie musimy czekać na wszystko.
Możemy zacząć wysyłać dokument, zanim render całego drzewa się zakończy.
Tak dochodzimy do Streaming SSR.