Konrad Kowalski (rootsher)Principal Platform & Reliability Architect010110100010001111011110011011111001000011010010

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:

jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

Do browsera może trafić:

html
<button>0</button>

To wystarczy, żeby coś wyświetlić.

Ale w tym HTML-u nie ma informacji o:

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

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

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

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

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

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

jsx
function Clock() {
  return <span>{Date.now()}</span>;
}

Serwer renderuje:

html
<span>1750000000000</span>

Kilkaset milisekund później browser wykonuje ten sam komponent:

html
<span>1750000000473</span>

Wyniki są różne.

Powstaje hydration mismatch.

Podobne problemy mogą powodować:

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

js
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ć:

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

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

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

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