Konrad Kowalski (rootsher)Principal Platform & Reliability Architect010100011111001101110101101011001101001110100011

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:

text
/products
  |
  v
server render
  |
  v
HTML document

/products/42
  |
  v
server render
  |
  v
another HTML document

W SPA:

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

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

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

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

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

text
GET /api/products/42
  |
  v
{
  "id": 42,
  "name": "Mechanical Keyboard",
  "price": 129
}

A browser wykonuje:

text
data
  |
  v
application state
  |
  v
render
  |
  v
DOM

To prowadzi do bardzo ważnego rozdzielenia:

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

js
{
  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.

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

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

js
history.pushState({}, "", "/products/42");

browser zmienia URL i historię, ale nie pobiera automatycznie nowego dokumentu.

To aplikacja musi zinterpretować nowy URL:

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

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

text
app.js - 400 KB

Ale 400 KB JavaScriptu kosztuje więcej niż samo pobranie.

Browser musi wykonać kolejne etapy:

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

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

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

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

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

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