Selective i progressive hydration: nie wszystko musi dostać CPU od razu
- data
- kategoria
- Frontend
- czytanie
- 3 min / 592 słów
Hydration rozwiązuje problem połączenia server-rendered HTML z działającą aplikacją po stronie klienta.
Problem w tym, że klasyczna hydration często traktuje całe drzewo jako jeden duży startup task.
Jeżeli strona ma dziesiątki interaktywnych regionów, browser może wykonać mnóstwo pracy zanim użytkownik w ogóle spróbuje z większości z nich skorzystać.
Naturalny kolejny krok brzmi:
nie hydratować wszystkiego naraz.
Hydration jako kolejka pracy
Załóżmy stronę:
header
hero
search
product filters
recommendations
reviews
newsletter
footer
Nie wszystkie elementy mają taki sam priorytet.
Search może być potrzebny natychmiast.
Reviews znajdują się trzy viewporty niżej.
Newsletter może nigdy nie zostać użyty.
Klasyczna hydration może jednak potraktować je podobnie:
load JS
|
v
recreate whole tree
|
v
attach behavior everywhere
Selective hydration próbuje zmienić ten model.
Najpierw to, co istotne
Zamiast jednego dużego zadania możemy myśleć o wielu mniejszych:
hydrate search
hydrate navigation
hydrate cart
hydrate reviews
hydrate newsletter
I nadać im priorytety.
Przykładowo:
visible + interactive
|
v
high priority
below the fold
|
v
low priority
never interacted with
|
v
maybe never needed
To zmienia hydration z operacji binarnej w problem schedulingu.
Selective i progressive to nie zawsze to samo
Terminologia różni się między frameworkami, ale użyteczny podział jest taki:
Progressive hydration oznacza stopniowe aktywowanie kolejnych części UI.
region A
|
v
region B
|
v
region C
Selective hydration oznacza możliwość wyboru, który region ma zostać aktywowany wcześniej, niezależnie od kolejności drzewa.
region A waiting
region B clicked by user
|
v
hydrate B first
To subtelna, ale ważna różnica.
W pierwszym modelu robimy pracę partiami.
W drugim scheduler może reagować na realny intent użytkownika.
Komercyjny przykład: portal contentowy
Wyobraźmy sobie homepage dużego portalu.
Na pierwszym ekranie znajdują się:
navigation
search
główny artykuł
Niżej:
komentarze
wyniki sportowe
rekomendacje
newsletter
interaktywny widget
Jeżeli użytkownik tylko czyta artykuł, natychmiastowa hydration wszystkich tych elementów jest czystym kosztem.
Lepsza strategia może wyglądać tak:
navigation -> immediately
search -> immediately
comments -> when visible
widget -> when visible
newsletter -> on interaction
Framework może implementować to automatycznie albo pozwolić developerowi definiować granice.
Visibility to tylko jeden sygnał
Najprostszy trigger:
component enters viewport
|
v
hydrate
Ale można wykorzystywać różne sygnały:
element staje się widoczny
użytkownik najeżdża kursorem
element dostaje focus
użytkownik zaczyna interakcję
browser ma idle time
zakończyło się ważniejsze zadanie
To daje model:
priority = f(
visibility,
user intent,
interaction,
available CPU
)
I to jest znacznie bliższe rzeczywistemu problemowi performance niż proste:
hydrate everything ASAP
Ciekawostka
Hydration konkuruje o main thread dokładnie tak samo jak każdy inny JavaScript.
Jeżeli browser ma do wykonania:
120 ms hydration task
a użytkownik kliknie po 20 ms, handler kliknięcia nie może po prostu „przerwać” wykonywanego JavaScriptu.
JavaScript na main threadzie działa kooperacyjnie.
Dopóki aktualny task nie odda kontroli:
hydration hydration hydration hydration
|
v
task ends
|
v
click handler
input czeka.
Dlatego dzielenie hydration na mniejsze jednostki może poprawić responsiveness nawet wtedy, gdy łączna ilość pracy pozostaje identyczna.
Mniej blokowania nie oznacza mniej pracy
To ważne rozróżnienie.
Załóżmy:
full hydration = 200 ms CPU
Możemy podzielić ją na:
20 ms
20 ms
20 ms
...
UI może wtedy reagować pomiędzy taskami.
Ale nadal wykonaliśmy 200 ms pracy.
Progressive hydration poprawia głównie:
kiedy praca blokuje użytkownika.
Nie musi zmniejszać całkowitego kosztu.
Żeby naprawdę wykonać mniej pracy, trzeba pójść krok dalej.
Hydrate on interaction
Załóżmy dropdown, którego większość użytkowników nigdy nie otworzy.
Zamiast:
page load
|
v
download dropdown JS
|
v
hydrate dropdown
możemy zrobić:
page load
|
v
static HTML
user interacts
|
v
load required JS
|
v
activate component
Tutaj korzyść jest większa.
Jeśli użytkownik nigdy nie kliknie, koszt może nigdy nie zostać poniesiony.
To jest ważna zmiana z:
do work later
na:
maybe never do the work
Ale interakcję trzeba jakoś przechwycić
Tu pojawia się ciekawy problem.
Jeżeli komponent nie jest jeszcze aktywny, to kto obsłuży pierwsze kliknięcie?
Potrzebujemy lekkiej warstwy, która potrafi:
capture interaction
|
v
identify required component
|
v
load code
|
v
activate component
|
v
replay / continue interaction
Czyli opóźnianie hydration nie usuwa infrastruktury.
Przenosi część odpowiedzialności do runtime'u odpowiedzialnego za lazy activation.
Granularność ma znaczenie
Możemy hydratować:
whole application
albo:
page sections
albo:
individual components
Im mniejsza granica, tym większa kontrola.
Ale też więcej:
punktów ładowania kodu
zależności
scheduling decisions
runtime bookkeeping
Nie ma sensu rozbijać aplikacji na tysiące granic tylko dlatego, że technicznie możemy.
Granice powinny odpowiadać realnej interaktywności.
Typowe rozwiązania
Współczesne frameworki realizują te idee na różne sposoby.
React rozwija mechanizmy selective hydration w połączeniu z concurrent rendering i Suspense.
Astro pozwala określać sposób aktywowania client components za pomocą strategii typu:
load immediately
load when visible
load when browser is idle
Nie jest jednak istotne, która biblioteka ma konkretną składnię.
Mechanizm pozostaje ten sam:
client CPU staje się zasobem, który trzeba przydzielać według priorytetów.
A co jeśli większość strony nigdy nie potrzebuje runtime'u?
Selective hydration nadal zakłada, że mamy server-rendered drzewo aplikacji i wybieramy moment aktywacji jego fragmentów.
Ale można odwrócić założenie.
Zamiast:
whole page is an application
some parts activate later
możemy powiedzieć:
whole page is static HTML
only some parts are applications
To brzmi podobnie, ale prowadzi do innej architektury.
Jednostką aplikacji przestaje być cała strona.
Stają się nią małe, niezależne wyspy interaktywności.
I właśnie z tego wyrasta Islands Architecture.