SPA i CSR: kiedy browser przejął aplikację
- data
- kategoria
- Frontend
- czytanie
- 4 min / 702 słów
AJAX rozwiązał jeden konkretny problem: request nie musiał już oznaczać pełnej nawigacji.
Kolejny krok był logiczny.
Skoro browser potrafi pobierać dane w tle i aktualizować tylko fragmenty UI, to dlaczego po każdym przejściu między ekranami serwer ma nadal renderować nowy dokument?
Tak zaczęła się era Single Page Applications.
Jeden dokument, wiele widoków
W klasycznym modelu:
/products
|
v
server render
|
v
HTML document
/products/42
|
v
server render
|
v
another HTML document
W SPA:
initial document
|
v
JavaScript application
|
v
client-side router
|
v
fetch data
|
v
render next view
Browser pobiera główny dokument raz, a później sam zarządza przejściami między ekranami.
URL nadal może się zmieniać, ale nie musi oznaczać pobrania nowego dokumentu.
To zmienia rolę frontendu fundamentalnie.
Frontend przestaje być warstwą prezentacji dokumentów.
Staje się długowiecznym runtime'em aplikacji.
CSR to nie to samo co SPA
Te pojęcia często są wrzucane do jednego worka, ale opisują różne rzeczy.
SPA mówi głównie o modelu nawigacji:
aplikacja działa w ramach jednego dokumentu.
CSR mówi o miejscu renderowania:
UI powstaje po stronie klienta.
Typowy model wygląda tak:
GET /
|
v
minimal HTML
|
v
download JavaScript
|
v
execute application
|
v
fetch data
|
v
render UI
Serwer nie musi już wygenerować gotowego widoku produktu.
Może zwrócić shell:
<div id="app"></div>
<script src="/app.js"></script>
A reszta dzieje się po stronie klienta.
Dlaczego to było atrakcyjne
Dla aplikacji takich jak poczta, CRM, dashboard czy edytor dokumentów model dokumentowy zaczyna być niewygodny.
Wyobraźmy sobie system analityczny.
Użytkownik:
zmienia zakres dat
otwiera modal
filtruje tabelę
przełącza zakładkę
rozwija wykres
zaznacza rekordy
aktualizuje dane w tle
Pełna nawigacja po każdej z tych operacji byłaby absurdalna.
SPA pozwala zachować:
application state
component state
cached data
open connections
current UI state
przez całą sesję.
To właśnie dlatego CSR nadal ma bardzo mocny sens w wielu aplikacjach po zalogowaniu.
Dla wewnętrznego panelu administracyjnego czas do pierwszego HTML-a może być mniej istotny niż płynność kolejnych setek interakcji.
Serwer zmienia rolę
W tym modelu backend coraz częściej przestaje zwracać widoki.
Zwraca dane:
GET /api/products/42
|
v
{
"id": 42,
"name": "Mechanical Keyboard",
"price": 129
}
A browser wykonuje:
data
|
v
application state
|
v
render
|
v
DOM
To prowadzi do bardzo ważnego rozdzielenia:
backend
-> data + business logic
frontend
-> application state + rendering
W praktyce nie zawsze granica jest tak czysta, ale właśnie wtedy zaczyna się utrwalać model API + frontend application.
Problem ręcznego synchronizowania DOM-u
Przy większej aplikacji bardzo szybko pojawia się trudny problem.
Masz stan:
{
user,
cart,
filters,
products,
selectedProduct,
notifications
}
I DOM, który ma być jego aktualną reprezentacją.
Jeżeli aktualizujesz DOM ręcznie, musisz pilnować wszystkich zależności.
state changes
|
v
which elements are affected?
|
v
which listeners survive?
|
v
which derived values change?
|
v
which DOM nodes need replacement?
To właśnie tutaj zaczynają rosnąć frameworki frontendowe.
Nie dlatego, że document.querySelector() jest za trudne.
Problemem jest utrzymanie spójności między zmieniającym się stanem a dużym drzewem UI.
React później opisze tę ideę bardzo elegancko:
UI = f(state)
Zamiast mówić browserowi dokładnie, które elementy ma zmienić, opisujesz, jak UI powinien wyglądać dla danego stanu.
Framework bierze na siebie synchronizację.
Ciekawostka
Client-side router nie „nawiguje” w tym samym sensie co browser.
Kiedy wykonujesz:
history.pushState({}, "", "/products/42");
browser zmienia URL i historię, ale nie pobiera automatycznie nowego dokumentu.
To aplikacja musi zinterpretować nowy URL:
URL changed
|
v
router matches route
|
v
load code/data
|
v
render view
To pozornie mały detal, ale oznacza ogromną zmianę odpowiedzialności.
W klasycznej nawigacji browser posiada protocol całego procesu.
W SPA duża część tego lifecycle'u staje się kodem aplikacji.
Koszt: browser dostaje coraz więcej pracy
CSR daje świetny model interakcji, ale przesuwa znaczną część kosztów na urządzenie użytkownika.
Typowa ścieżka pierwszego renderu może wyglądać tak:
HTML
|
v
discover JS
|
v
download JS
|
v
parse
|
v
compile
|
v
execute
|
v
initialize framework
|
v
render
|
v
discover required data
|
v
request data
|
v
render again
I właśnie tutaj pojawia się problem, którego początkowo łatwo nie zauważyć.
Sieć może dostarczyć dokument bardzo szybko, ale użytkownik nadal patrzy na pusty shell.
Download to tylko część kosztu JavaScriptu
Frontendowcy długo patrzyli głównie na rozmiar bundle'a.
Na przykład:
app.js - 400 KB
Ale 400 KB JavaScriptu kosztuje więcej niż samo pobranie.
Browser musi wykonać kolejne etapy:
download
|
v
decompress
|
v
parse
|
v
compile
|
v
execute
Na mocnym laptopie może to być prawie niewidoczne.
Na słabszym telefonie różnica robi się znacząca.
Do tego JavaScript konkuruje o main thread z:
input
layout
paint
inne taski
Dlatego dwie strony z identycznym transfer size mogą mieć zupełnie inne startup performance.
CSR może też tworzyć waterfall
Załóżmy, że HTML zawiera tylko:
<div id="app"></div>
<script src="/app.js"></script>
Browser nie wie jeszcze, że aplikacja potrzebuje /api/products.
Dowiedzenie się o tym wygląda tak:
HTML
|
v
app.js
|
v
execute
|
v
discover data dependency
|
v
/api/products
Każda strzałka może oznaczać kolejne latency.
Im później odkrywamy dependency, tym później możemy zacząć ją pobierać.
To będzie później jednym z powodów powrotu części renderowania na serwer.
Komercyjnie CSR nadal ma dużo sensu
Wyobraźmy sobie narzędzie podobne do Figmy, Notiona albo rozbudowanego panelu tradingowego.
Po wejściu do aplikacji użytkownik może pracować przez godzinę.
W takim systemie koszt początkowego uruchomienia można zaakceptować, jeśli później otrzymujemy:
natychmiastowe przejścia między widokami
lokalny stan
optimistic updates
background synchronization
intensywną interaktywność bez navigation
Dlatego CSR nie jest „starym modelem, który SSR zastąpił”.
To nadal sensowna architektura dla konkretnej klasy aplikacji.
Problem pojawia się wtedy, gdy zaczynamy stosować ją wszędzie.
Landing page, artykuł, listing produktów i panel administracyjny nie mają tych samych właściwości.
A web około połowy lat 2010 zaczął coraz mocniej odczuwać koszt podejścia:
ship everything
-> execute everything
-> render everything
-> then show content
I wtedy wahadło zaczęło wracać w stronę serwera.
Nie po to, żeby zabić SPA.
Po to, żeby odzyskać jedną rzecz, którą klasyczny web miał od początku:
HTML dostępny zanim aplikacja JavaScriptowa zdąży się uruchomić.