Hydration: jak połączyć HTML z działającą aplikacją
- data
- kategoria
- Frontend
- czytanie
- 3 min / 593 słów
SSR rozwiązuje problem pierwszego renderu.
Serwer może wygenerować HTML wcześniej, dzięki czemu użytkownik zobaczy treść zanim cały JavaScript aplikacji zdąży się uruchomić.
Ale sam HTML nie jest jeszcze aplikacją.
Przycisk może wyglądać jak przycisk, formularz może wyglądać jak formularz, a menu jak menu, ale jeśli interakcja zależy od JavaScriptu, browser musi jeszcze odtworzyć runtime aplikacji.
Ten proces nazywamy hydration.
HTML to tylko wynik renderowania
Załóżmy, że serwer renderuje komponent:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Do browsera może trafić:
<button>0</button>
To wystarczy, żeby coś wyświetlić.
Ale w tym HTML-u nie ma informacji o:
useState
funkcja setCount
callback onClick
struktura komponentów
zależności runtime'u
Browser widzi element DOM.
Nie widzi komponentu.
Hydration rekonstruuje warstwę aplikacji
Po załadowaniu JavaScriptu framework musi odtworzyć strukturę, która wygenerowała HTML.
W uproszczeniu:
server
Component Tree
|
v
render
|
v
HTML
|
v
network
|
v
browser
existing DOM
+
JavaScript
|
v
recreate component tree
|
v
connect runtime with DOM
|
v
interactive application
Istotne jest to, że framework zwykle nie chce ponownie tworzyć całego DOM-u.
DOM już istnieje.
Hydration próbuje wykorzystać istniejące elementy i powiązać je z client-side runtime'em.
Dlatego nazwa jest całkiem trafna: statyczny HTML zostaje „nawodniony” zachowaniem aplikacji.
SSR nie oznacza więc mniej pracy
To częsty błąd w mental modelu.
Można założyć:
CSR:
browser renders
SSR:
server renders
i dojść do wniosku, że SSR po prostu przenosi pracę z klienta na serwer.
W klasycznej aplikacji hydratowanej sytuacja jest bliższa:
SSR:
server render
+
client reconstruction
Serwer wykonał render po to, żeby użytkownik wcześniej dostał HTML.
Browser nadal musi jednak uruchomić aplikację.
Część pracy została więc dodana, a nie przeniesiona.
To może być świetny trade-off, ale tylko wtedy, gdy wcześniejsze pokazanie treści jest warte dodatkowego kosztu.
Komercyjny przykład: sklep wygląda szybko, ale nie reaguje
Wyobraźmy sobie product detail page.
Serwer wysyła:
<h1>Mechanical Keyboard</h1>
<p>129 zł</p>
<button>Add to cart</button>
Treść pojawia się bardzo szybko.
Użytkownik widzi button i od razu go klika.
Problem: JavaScript aplikacji jeszcze się ładuje albo main thread jest zajęty hydration.
Wizualnie strona wygląda na gotową.
Funkcjonalnie jeszcze nie jest.
To jeden z najbardziej zdradliwych failure modes SSR:
visible
!=
interactive
Dla użytkownika nie ma znaczenia, że Lighthouse chwali szybki pierwszy render, jeśli kliknięcie przez chwilę nie daje żadnego efektu.
Hydration musi zgadzać się z serwerem
Jest jeszcze jeden problem.
Client-side render powinien odtworzyć strukturę zgodną z HTML-em wygenerowanym na serwerze.
Załóżmy:
function Clock() {
return <span>{Date.now()}</span>;
}
Serwer renderuje:
<span>1750000000000</span>
Kilkaset milisekund później browser wykonuje ten sam komponent:
<span>1750000000473</span>
Wyniki są różne.
Powstaje hydration mismatch.
Podobne problemy mogą powodować:
losowe wartości
lokalna strefa czasowa
locale
dane dostępne tylko w browserze
warunki zależne od window
różny state po obu stronach
SSR wymaga więc czegoś więcej niż możliwości uruchomienia tego samego komponentu na serwerze i kliencie.
Wymaga odpowiednio deterministycznego pierwszego renderu.
Ciekawostka
HTML nie potrafi serializować closure.
Załóżmy:
const productId = 42;
button.onclick = () => {
addToCart(productId);
};
Serwer nie może wysłać funkcji onclick jako części normalnego HTML-a razem z jej lexical environment.
Może wysłać:
<button>Add to cart</button>
ale informacja o tym, że kliknięcie ma wywołać addToCart(42), należy do świata JavaScript runtime.
To jeden z fundamentalnych powodów, dla których server-rendered HTML nie może po prostu „zawierać działającej aplikacji”.
Trzeba albo odtworzyć execution context po stronie klienta, albo znaleźć sposób na zakodowanie wystarczającej ilości informacji, żeby później go wznowić.
Ta druga możliwość stanie się ważna przy resumability.
Im większa strona, tym większy koszt
Wyobraźmy sobie portal z:
header
menu
artykuł
komentarze
related content
reklamy
newsletter
widget pogody
panel użytkownika
Jeżeli wszystko jest częścią jednego hydratowanego drzewa, browser może potrzebować uruchomić dużą ilość JavaScriptu nawet wtedy, gdy użytkownik chce tylko przeczytać artykuł.
To prowadzi do pytania:
czy wszystko naprawdę musi być interaktywne od razu?
Jeśli komentarze znajdują się trzy ekrany niżej, może nie warto hydratować ich podczas startupu.
Jeśli footer nie ma żadnej interakcji, może w ogóle nie potrzebuje client runtime'u.
I właśnie z tego pytania wyrastają kolejne strategie:
progressive hydration
selective hydration
partial hydration
islands architecture
Hydration to problem schedulingu
Można patrzeć na hydration jak na technikę frameworkową.
Ciekawszy model brzmi jednak:
mamy pewną ilość pracy JavaScriptowej do wykonania na ograniczonym main threadzie.
Ta praca konkuruje z:
user input
layout
paint
network callbacks
application logic
Dlatego prawdziwe pytanie nie brzmi tylko:
jak szybko zrobić hydration?
Ale również:
którą część aplikacji naprawdę musimy hydratować teraz?
To przesuwa dyskusję z samego renderowania w stronę priorytetów, schedulingu i granularności interaktywności.
I właśnie tutaj zaczyna się następny etap ewolucji architektur renderowania.