Islands architecture: interaktywne wyspy w statycznym dokumencie
- data
- kategoria
- Frontend
- czytanie
- 3 min / 588 słów
Selective hydration zadaje pytanie:
które części aplikacji aktywować wcześniej, a które później?
Islands Architecture idzie krok dalej i zmienia punkt wyjścia.
Nie zakłada już, że cała strona jest aplikacją.
Zakłada raczej:
strona jest dokumentem, a tylko wybrane fragmenty są aplikacjami.
Static by default
Wyobraźmy sobie portal contentowy:
article
author bio
table of contents
comments
newsletter
stock ticker
related content
W klasycznym SPA wszystko może należeć do jednego client-side runtime'u.
W modelu islands:
STATIC DOCUMENT
├── article
├── author bio
├── table of contents
├── [comments island]
├── [newsletter island]
├── [stock ticker island]
└── related content
Większość strony pozostaje zwykłym HTML-em.
JavaScript trafia tylko tam, gdzie jest realnie potrzebny.
Wyspa jest granicą interaktywności
To najważniejszy mental model.
Nie chodzi tylko o code splitting.
Możemy mieć dwa komponenty ładowane w osobnych chunkach, ale nadal należące do jednego globalnego runtime'u aplikacji.
Island oznacza mocniejszą granicę:
static HTML
│
├── interactive runtime A
│
└── interactive runtime B
Każdy interaktywny fragment może zostać aktywowany niezależnie.
Reszta dokumentu nie wymaga hydration.
Komercyjny przykład: portal finansowy
Załóżmy stronę artykułu o wynikach spółki.
Większość contentu:
tytuł
tekst
grafiki
tabela danych
bio autora
jest statyczna.
Interaktywne są tylko:
live stock ticker
wykres
komentarze
newsletter
W klasycznej aplikacji komponentowej cały route może zostać objęty hydration.
W islands:
article -> static
author -> static
chart -> island
ticker -> island
comments -> island
newsletter -> island
Użytkownik dostaje mniej JavaScriptu i mniej pracy startupowej.
Dla content-heavy produktów to często bardzo dobry trade-off.
Islands to nie po prostu lazy loading
Lazy loading mówi:
pobierz kod później.
Islands mówi:
ten fragment ma własną granicę client runtime'u.
To różnica między:
one application
|
v
lazy component
a:
document
|
v
independent interactive regions
Lazy loading jest więc mechanizmem delivery.
Islands Architecture jest decyzją o strukturze aplikacji.
Ciekawostka
Granica island nie musi pokrywać się z granicą komponentu.
Możemy mieć:
<ProductCard>
<Image />
<Title />
<Price />
<FavoriteButton />
</ProductCard>
ale tylko:
<FavoriteButton />
wymaga JavaScriptu.
Z perspektywy organizacji kodu ProductCard może być jednym komponentem.
Z perspektywy runtime'u tylko mały fragment musi być island.
To pokazuje ważne rozróżnienie:
component boundary
!=
bundle boundary
!=
hydration boundary
Framework może je czasem łączyć dla wygody, ale architektonicznie są to trzy różne decyzje.
Każda wyspa może mieć inny moment aktywacji
Nie wszystkie islands muszą startować natychmiast.
Możemy mieć:
search
-> activate on load
chart
-> activate when visible
newsletter
-> activate on interaction
comments
-> activate when user scrolls near section
To łączy islands z wcześniejszym problemem schedulingu.
Różnica polega na tym, że teraz część strony nie ma w ogóle nic do hydratowania.
Mniej JavaScriptu to nie jedyna korzyść
Najbardziej oczywista zaleta brzmi:
less client JS
Ale równie ważna jest izolacja.
Jeżeli niezależne regiony mają własne granice, łatwiej ograniczyć:
zakres rerenderów
ilość współdzielonego state
startup dependencies
blast radius błędu
To może uprościć system.
Ale tylko do pewnego momentu.
Shared state robi się trudniejszy
Załóżmy dwie wyspy:
[cart counter]
...
[add to cart]
Obie muszą znać stan koszyka.
Jeśli należą do jednej aplikacji:
global store
|
v
component A
component B
problem jest prosty.
Przy niezależnych islands trzeba zdecydować, jak synchronizować stan:
island A
^
v
shared channel?
^
v
island B
Możliwości jest wiele:
browser events
shared store
URL
server state
custom runtime
BroadcastChannel w bardziej specyficznych przypadkach
Ale każda metoda ponownie zwiększa coupling.
Im więcej shared state między wyspami, tym bardziej system zaczyna przypominać jedną aplikację, którą wcześniej próbowaliśmy rozdzielić.
Dużo islands może oznaczać dużo runtime'ów
Kolejny problem to dependencies.
Jeżeli kilka islands używa tego samego frameworka, chcemy uniknąć sytuacji:
island A -> framework runtime
island B -> framework runtime
island C -> framework runtime
gdzie ten sam kod jest wielokrotnie pobierany lub inicjalizowany.
Nowoczesne tooling potrafi współdzielić dependency, ale architektura nadal wymaga sensownej granularności.
Sto małych islands nie musi być lepsze niż pięć większych.
Typowe rozwiązania
Najbardziej bezpośrednim współczesnym przykładem jest Astro, które domyślnie renderuje komponenty do HTML-u i wysyła client-side JavaScript tylko dla elementów jawnie oznaczonych jako interaktywne.
Możemy koncepcyjnie powiedzieć:
server component
-> HTML only
client island
-> HTML + JS runtime
Podobne idee istniały jednak wcześniej i nie są własnością konkretnego frameworka.
Astro po prostu uczyniło ten model centralnym elementem swojej architektury.
Islands mają świetny sweet spot
Ten model szczególnie dobrze pasuje do aplikacji, które są głównie dokumentami:
media
blogi
dokumentacja
marketing
e-commerce content pages
portale informacyjne
Gorzej pasuje do aplikacji, gdzie prawie wszystko współdzieli intensywny state:
Figma
complex editor
trading terminal
IDE
Tam próba podzielenia systemu na niezależne islands może stworzyć więcej komunikacji niż korzyści.
Nadal jednak wykonujemy hydration
To kluczowe.
Island ogranicza zakres hydration:
whole page
|
v
NO
selected region
|
v
YES
Ale wewnątrz wyspy nadal często dzieje się klasyczny proces:
server HTML
|
v
download JS
|
v
reconstruct component
|
v
hydrate
Zmniejszyliśmy problem.
Nie usunęliśmy go.
A skoro serwer już wykonał komponent i zna jego stan, pojawia się bardziej radykalne pytanie:
czy browser naprawdę musi rekonstruować ten komponent od początku?
Może dałoby się wysłać wystarczająco dużo informacji, żeby klient po prostu kontynuował wykonanie dokładnie tam, gdzie serwer je zostawił.
To prowadzi do Resumability.